1#
2# ver : 1.2 || date: Oct 17, 2018 || auth: PA Taylor
3# + separate title and text strings
4# + new warn type
5#
6# ver : 1.3 || date: Nov 1, 2018
7# + [PT] wrap_imag now includes href, so clicking on image opens it in
8#   new link
9#
10# ver : 1.4 || date: Nov 1, 2018
11# + [PT] Making py3 compatible:
12#        from 2to3, updating DICT.has_key(x) -> x in DICT
13#
14# ver : 1.5 || date: Feb 21, 2019
15# + [PT] adding comment to empty button sets it to "?", not "X"
16#
17#ver = '2.21' ; date = 'May 17, 2019'
18# + [PT] simplifying radcor behavior
19#
20#ver = '2.3' ; date = 'July 3, 2019'
21# + [PT] Colorbars standard widths
22# + [PT] QC block ID now in QC block titles
23# + [PT] added more help descriptions
24#
25#ver = '2.4' ; date = 'March 27, 2020'
26# [PT] remove dependency on lib_apqc_html_helps.py
27#    + absorb those lahh.* functions and variables here
28#
29ver = '2.5' ; date = 'Feb 23, 2021'
30# [PT] update helps, reorder
31#
32#########################################################################
33
34
35# mostly ways to read & wrap text & images.
36import sys
37import os
38import json
39import collections as coll
40
41from afnipy import lib_apqc_html_css   as lahc
42
43NULL_BTN1     = '' # empty space, used to keep button populated
44NULL_BTN0     = '|' # empty space, used to keep button populated
45
46json_cols  = [ 'qcele', 'rating', 'comment' ]
47PBAR_FLAG  = 'SHOW_PBAR'
48dir_img    = 'media'
49
50## ==========================================================================
51## ==========================================================================
52## ==========================================================================
53## ==========================================================================
54## ==========================================================================
55
56# --------- Section on what used to be library of "apqc html helps" ---------
57
58# For single subj QC, we now identify major groupings/categories to
59# look at: QC blocks.  Those will determine general organization
60# (sections, section titles, etc.) and guide usage.
61#
62# Format of each entry:
63# qc_block[ ABBREV ] = [ "SECTION HOVER TEXT", "SECTION TITLE" ]
64#
65# For organizational purposes, these quantity abbrevs are/should be
66# used in the names of functions used to generate each section. (Makes
67# it easier to see what is connected to what, even if they change.)
68
69qc_title           = coll.OrderedDict()
70qc_title["Top"]    = [ "Top of page for:&#10${subj}",
71                       "afni_proc.py single subject report" ]
72
73qc_blocks          = coll.OrderedDict()
74qc_blocks["vorig"]  = [ "vols in orig space",
75                        "Check vols in original space" ]
76
77qc_blocks["ve2a" ]  = [ "vol alignment (EPI-anat)",
78                        "Check vol alignment (EPI to anat)" ]
79
80qc_blocks["va2t" ]  = [ "vol alignment (anat-template)",
81                        "Check vol alignment (anat to template)" ]
82
83qc_blocks["vstat"]  = [ "statistics vols",
84                        "Check statistics vols (and effect estimates)" ]
85
86qc_blocks["mot"  ]  = [ "motion and outliers",
87                        "Check motion and outliers" ]
88
89qc_blocks["regr" ]  = [ "regressors",
90                        "Check regressors, DFs and residuals" ]
91
92qc_blocks["radcor"] = [ "@radial_correlate vols",
93                        "Check extent of local correlation" ]
94
95qc_blocks["warns"]  = [ "all warnings from processing",
96                        "Check all warnings from processing" ]
97
98qc_blocks["qsumm"]  = [ "summary quantities from @ss_review_basic",
99                        "Check summary quantities from @ss_review_basic" ]
100
101qc_link_final       = [ "FINAL",
102                       "overall subject rating" ]
103
104# ------------------------------
105
106# a brief help string for online help
107qcb_helps           = coll.OrderedDict()
108qcb_helps["vorig"]  = '''
109Volumetric mages of data (EPI and anat) in original/native space.
110
111(EPI should now be shown;  anat vol will be along shortly.)
112'''
113
114qcb_helps["ve2a" ]  = '''
115Volumetric images of the alignment of the subject's anat
116(underlay/grayscale) and EPI (overlay/hot color edges) volumes. Likely
117these will be shown in the template space, if using the tlrc block.
118'''
119
120qcb_helps["va2t" ]  = '''
121Volumetric images of the alignment of the standard space template
122(underlay/grayscale) and subject's anat (overlay/hot color edges)
123volumes.
124'''
125
126qcb_helps["vstat"]  = '''
127Volumetric images of statistics results (and, where available, effect
128estimates).  These images are only created for task data sets, i.e.,
129where GLTs or stimuli are specified (so not for resting state data).
130
131By default, the (full) F-stat of an overall regression model is shown.
132Additionally, one can specify labels of stimuli or GLTs used in the
133afni_proc.py command, and statistical results will be shown.  For
134stimuli with effect estimates, the 'Coef' vales will be displayed as
135the olay colors (preferably with the 'scale' block having been used in
136afni_proc.py, producing meaningful units of BOLD % signal change in
137the 'Coef' volumes).
138
139Colorbar ranges and thresholds are chosen from either percentile
140values within the data set (preferably from within a WB mask,
141available when the 'mask' block was used in afni_proc.py) or from
142pre-set statistical levels (like p=0.001).  Each is case is described.
143
144'''
145
146qcb_helps["mot"  ]  = '''
147Summary of motion and outlier information, which may each/both be
148used as censoring criteria.
149
150The 6 rigid body motion parameters (3 rotation + 3 translation) are
151combined into a single quantity: the Euclidean norm (enorm), which has
152approx. units of 'mm'.  Large changes in the enorm time series show
153moments of subject motion.
154
155Separate runs are shown with the background alternating between white
156and light gray.
157
158Boxplots summarize parameter values, both before censoring (BC) and
159after censoring (AC).
160
161And a grayplot of residuals (with motion/outliers/censoring) is
162provided.  The '-pvorder' is used for output, placing the time series
163in decreasing order of similarity to the top two principal components
164of the (masked) time series data.  The colorbar max is set to 3.29,
165the value at which a standard normal distribution N(0,1) has a
166two-sided tail probability of 0.001.  The grayplot's top row contains
167a plot of the motion enorm and outlier frac across time, for reference
168with the grayplot series.
169'''
170
171qcb_helps["regr" ]  = '''
172When processing with stimulus time series, both individual and
173combined stimulus plots are generated (with any censoring also shown).
174
175The degrees of freedom (DF) summary is also provided, so one can check
176if too many get used up during processing (careful with bandpassing!).
177
178The "corr_brain" plot shows correlation of each voxel with the errts
179average within the whole brain mask (what could be called the 'global
180signal').
181
182Two TSNR dsets can be shown.  In each case, voxelwise TSNR is shown
183throughout the full FOV, and any brain mask dset is just used for
184defining a region within which percentiles are calculated. The generic
185formula for TSNR is:
186            TSNR = average(signal) / stdev(noise)
187+ First, the TSNR of r01 after volreg is shown if the user used the
188  '-volreg_compute_tsnr yes' opt in AP. Here, the "signal" is the time
189  series and the "noise" is the detrended time series.
190+ Second, the TSNR of the combined runs after regression modeling is
191  shown. Here, the "signal" is the all_runs dset and the "noise" is
192  the errts time series.
193When a mask is present, the olay's hot colors (yellow-orange-red) are
194defined by the 5-95%ile range of TSNR in the mask.  The 1-5%ile values
195within the mask are shown in light blue, and the lower values are
196shown in dark blue.  In the absence of a mask, then the colorbar goes from
1970 to the 98%ile value within the whole dset.
198'''
199
200qcb_helps["radcor"] = '''
201@radial_correlate plots (per run, per block). These can show
202scanner coil artifacts, as well as large subject motion; both factors
203can lead to large areas of very high correlation, which would be
204highlighted here.
205'''
206
207qcb_helps["warns"]  = '''
208Several AFNI programs carry out consistency checks while
209processing (e.g., pre-steady state check, regression matrix corr
210warnings, left-right flip checks).  Warnings are conglomerated here.
211
212Each warning has one of the following levels:
213    {}
214
215The warning level is written, with color coding, at the top of each
216warning's text box.  The QC block label 'warns' at the top of the page
217is also colored according to the maximum warning level present.
218'''.format( lahc.wlevel_str )
219
220qcb_helps["qsumm"]  = '''
221This is the output of @ss_review_basic, which contains a loooot of
222useful information about your single subject processing.
223'''
224
225qcbh = ''
226for x in qcb_helps.keys():
227    y    = qcb_helps[x]
228    yspl = y.split('\n')
229    yind = '\n    '.join(yspl)
230    qcbh+= '\n' + x
231    qcbh+= yind
232
233
234# -----------------------------------------------------------------------
235# -----------------------------------------------------------------------
236
237apqc_help = [
238['HELP FILE FOR AP-QC',
239 '''afni_proc.py's single subject QC report form'''],
240['OVERVIEW',
241'''QC organization
242    The quality control (QC) is organized into thematic blocks to
243    check, such as original data acquisition, different alignments,
244    motion, regression modeling, and more. At the top of the QC page,
245    there is a navigation bar with a label for each QC block (vorig,
246    ve2a, etc.) that functions as a button to the top of that section.
247
248Rating
249    Beneath each section label is a(n initially empty) QC button,
250    which users can click to set a rating for that QC block and for
251    the overall subject rating ("FINAL"). Clicking on the QC button
252    toggles its state through good (+), bad (X) or other (?, which may
253    include 'ugly', or just a hint to revisit later).  Users can also
254    use convenient 'filler buttons' at the right (A+, Ax, etc.), when
255    the ratings are constant/uniform-- one hopes for 'all good'
256    processing, but who knows...
257
258Commenting
259    Additionally, users can ctrl+click on the QC button to enter a
260    comment for that block.  For example, they can write why a rating
261    was good or bad, or what question they have led them to rate it as
262    'other'.
263
264Saving
265    Clicking SAVE will let the user save the QC ratings+comments on
266    their computer for later use, such as inclusion/exclusion criteria
267    for the subject in group analysis.  (NB: This action is treated
268    the same as downloading a file from online, and is subject to
269    standard limitations on simplicity due to browser security
270    settings.)
271
272See also
273    The online web tutorial:
274    https://afni.nimh.nih.gov/pub/dist/doc/htmldoc/tutorials/apqc_html/main_toc.html
275    It's more verbose and pictorial, if that's useful.
276'''
277],
278['''DEFINITIONS''',
279'''QC block
280    One thematic section of the QC form (original data, alignment
281    step, etc.).  Each block has a label in the navigation bar (vorig,
282    ve2a, etc.)-- click on the label to jump to that block. Click on
283    the button below the label to provide a rating for that block.
284
285QC button
286    Below each QC block label in the navigation bar is a button
287    (initially empty after running afni_proc.py). The user can click
288    on it to toggle its state to one of three ratings (good, +; bad,
289    X; other, ?), as well as to enter a comment for that QC block (via
290    ctrl+click).
291
292'FINAL'
293    Label for the QC button to hold the user's overall/final
294    evaluation of the subject's data and processing. (Clicking on this
295    label does nothing.)
296
297filler button
298    |A+|, |Ax|, |A?|-- located in the upper right corner of the
299    navigation menu.  These can be used to provide uniform ratings
300    across the QC buttons (click to fill empty buttons, double-click
301    to fill *all* buttons, regardless of state).
302    |clr|-- double-clicking on this will empty all ratings+comments
303    from the QC buttons.
304
305'SAVE'
306    Write the ratings+comments to disk.  This action is done through
307    the browser, and is subject to the browser settings; probably
308    users should not have the browser set to automatically download
309    all files to the same location, for example.
310
311'HELP'
312    Button : well, how did you get here??
313
314'BC', 'AC'
315    When using the 'pythonic' HTML style, 1D-plotting (like motion
316    enorm plots) also produces boxplots to summarize values.  When
317    censoring has been applied, by default there will be two boxplots
318    made: one of values 'before censoring' (BC), and one of values
319    'after censoring' (AC).  Those labels are affixed in order to the
320    titles of the boxplots.
321
322'''],
323['''SET BUTTON RATING''',
324'''
325To record an evaluation, click the button below any section label, and
326toggle through:
327    + : good,
328    X : bad,
329    ? : other (or 'revisit').
330
331For speed, you can click 'filler button' |A+| once to fill all *empty*
332buttons with +, or doubly to fill *all* buttons with +.  |Ax| behaves
333the same for X, and |A?| for ?.
334
335Double-click |clr| to clear all rating and comment values.
336
337Pro-tip: if data are mostly all in a single state like good or bad,
338just use filler buttons to save yourself click time, and then just
339click any individual buttons that are different.  '''],
340['''COMMENT''',
341'''
342Use ctrl+click on a QC button to open (or close) a comment window.
343
344Save a comment with the green (left) button, or hit Enter at any point.
345Remove a comment with the pink (right) button, or hit Esc at any point.
346
347Any QC button with a comment gets a pair of quotes added, like ''+''.
348Comments are independent of rating, but adding a comment to an empty
349button changes its rating to ''?'' (which can be altered further from
350there).
351'''],
352['''SAVE FORM''',
353'''Click on the 'SAVE' button.
354
355Unfortunately, the file will not be directly saved by this, due to
356security settings on most web-browsers, and the user will be prompted
357to save the file as if downloading from the Web.'''
358],
359['''KEYBOARD NAVIGATION''',
360'''
361Use Tab to navigate the QC menu mirroring all above functionality.
362
363Hit Tab to move through the menu.  Hit Enter on a section label to
364scroll the page there.
365
366On QC buttons hit Enter to toggle through the rating list.  Use
367ctrl+Enter to open comments; as above, use Enter or Esc to keep or
368erase, respectively.
369
370On the filler buttons |A+|, |Ax| and |A?|, use Enter to fill empty
371QC buttons and ctrl+Enter to fill *all* buttons.
372On |clr|, ctrl+Enter clears all rating and comment values.
373'''],
374['''QC BLOCKS''',
375qcbh ]
376]
377
378# ---------------------------------------------------------------------
379
380brate_hover = '''QC BUTTON FORM
381
382NAVIGATE
383Click a label ('Top', etc.) to jump to a section.
384
385RATE + COMMENT
386Click the QC button below it to record your rating, toggling through:
387    X  :  bad,
388    ?  :  other/revisit,
389    +  :  good.
390Use ctrl+click on a QC button to provide a comment.  Close the comment
391panel with ctrl+click or its buttons.
392
393SPEEDIFY
394There are 'filler buttons' for each rating: |A+|, |Ax|, |A?|.
395Click once to fill all *empty* buttons with that rating, or
396double click to fill *all* buttons with that rating.
397
398--- click 'HELP' at the right for more details and features ---'''
399
400# ---------------------------------------------------------------------
401
402bgood_hover = '''Speed rating with:  +.
403Click once (or Enter) to fill all *empty* QC buttons;
404Double-click (or ctrl+Enter) to fill *all* QC buttons.
405'''
406
407bbad_hover = '''Speed rating with:  X.
408Click once (or Enter) to fill all *empty* QC buttons;
409Double-click (or ctrl+Enter) to fill *all* QC buttons.
410'''
411
412bother_hover = '''Speed rating with:  ?.
413Click once (or Enter) to fill all *empty* QC buttons;
414Double-click (or ctrl+Enter) to fill *all* QC buttons.
415'''
416
417bclear_hover = '''Clear all (!) QC buttons.
418Double-click (or ctrl+Enter) to clear *all* QC buttons.
419'''
420
421# ---------------------------------------------------------------------
422
423bsave_hover = '''Save ratings+comments.
424Click once (or Enter) to write QC form to disk.
425'''
426
427bhelp_hover = '''Open help page.
428Click once (or Enter) to open new help page.
429'''
430
431
432
433def write_help_html_file( ofile, ocss ):
434
435    # -------------- start ---------------------
436
437    ht  = '''
438    <html>
439    '''
440
441    # -------------- head ---------------------
442
443    # use same style sheet as main pages
444    ht+= '''
445    <head>
446    <link rel="stylesheet" type="text/css" href="{}" />
447    '''.format( ocss )
448
449    ht+= '''
450    </head>
451    '''
452
453    # -------------- body ---------------------
454
455    ht+= '''
456    <body>
457    '''
458
459    Nsec = len(apqc_help)
460
461    for ii in range(Nsec):
462        x = apqc_help[ii]
463
464        ht+= wrap_block_title( x[0],
465                               vpad=1,
466                               addclass=" class='padtop' ",
467                               blockid='' )
468
469        ht+= wrap_block_text( x[1],
470                              addclass=" class='container' " )
471
472
473
474    # -------------- finish ---------------------
475
476    ht+= '''</body>'''
477    ht+= '''</html>'''
478
479    # -------------- output ---------------------
480
481    fff = open( ofile, "w" )
482    fff.write( ht )
483    fff.close()
484
485# ----------------------------------------------------------------------
486# ----------------------------------------------------------------------
487# ----------------------------------------------------------------------
488# ----------------------------------------------------------------------
489# ----------------------------------------------------------------------
490
491# -------------- functions for main HTML file generation ---------------
492
493def parse_apqc_text_fields(tt) :
494    '''Input: tt can be either a string or list of strings.
495
496    Output: a single string, concatenation of the list (if input as
497    such).  Special case parsing happens if one string contains a flag
498    to include a PBAR in the string.
499    '''
500
501    out = ""
502
503    # not all versions of python seem to have 'unicode' type
504    try:
505        if type(tt) == unicode :
506            tt = [str(tt)]
507    except:
508        if type(tt) == str :
509            tt = [tt]
510
511    Ntt = len(tt)
512
513    for ii in range(Ntt) :
514        ss = tt[ii]
515        #print(ss)
516        if ss.__contains__(PBAR_FLAG) :
517            sspbar = make_inline_pbar_str(ss)
518            out+= sspbar
519        else:
520            out+= ss
521        if ii < (Ntt - 1) :
522            out+= "\n"
523
524    return out
525
526# -------------------------------------------------------------------
527
528def make_inline_pbar_str(ss) :
529    '''Input: ss is single string of form:  {PBAR_FLAG}:{pbar.json}
530
531    Output: a single string, formatted to put pbar inline in text, and
532    possibly have a second line of thr info.
533
534    '''
535
536    add_indent = ss.find(PBAR_FLAG)
537    if add_indent >= 0 :
538        str_indent = add_indent * ' '
539    else:
540        sys.error("** ERROR: missing pbar flag in {}".format(ss))
541
542    # file name of JSON should be only thing after ":"
543    fname_json = dir_img + "/"  + ss.split(":")[1]
544
545    # pbar dict keys given in @chauffeur_afni
546    with open(fname_json, 'r') as fff:
547        pbar_dict = json.load(fff)
548
549    if not(pbar_dict.__contains__('pbar_for')) :
550        pbar_dict['pbar_for'] = 'olay'
551
552    out = str_indent + ''' {pbar_for}: {pbar_bot} <img  class='pbar'  style="display: inline;" src="media/{pbar_fname}" > {pbar_top}'''.format(**pbar_dict)
553
554    if pbar_dict['pbar_comm'] :
555        out+= ''' ({pbar_comm})'''.format(**pbar_dict)
556
557    if pbar_dict['vthr'] :
558        out+= '''\n'''
559        out+= str_indent + ''' thr : {vthr}'''.format(**pbar_dict)
560
561        if pbar_dict['vthr_comm'] :
562            out+= ''' ({vthr_comm})'''.format(**pbar_dict)
563
564    if pbar_dict['gen_comm'] :
565        out+= '''\n'''
566        out+= str_indent + '''{gen_comm}'''.format(**pbar_dict)
567
568    return out
569
570
571# -------------------------------------------------------------------
572
573# these are the properties/fields that the incoming text might have.
574# These define what fields the jsons created by @ss_review_html (and
575# therefore, ultimately in lib_apqc_tcsh.py) can have.
576class apqc_item_info:
577
578    title       = ""
579    text        = ""
580    subtext     = ""
581    itemtype    = ""
582    itemid      = ""
583    blockid     = ""
584    blockid_hov = ""
585    warn_level  = ""   # [PT: May 21, 2019]
586
587    def set_title(self, DICT):
588        if 'title' in DICT :
589            self.title = DICT['title']
590
591    def set_itemtype(self, DICT):
592        if 'itemtype' in DICT :
593            self.itemtype = DICT['itemtype']
594
595    def set_itemid(self, DICT):
596        if 'itemid' in DICT :
597            self.itemid = DICT['itemid']
598
599    def set_blockid(self, DICT):
600        if 'blockid' in DICT :
601            self.blockid = DICT['blockid']
602
603    def set_blockid_hov(self, DICT):
604        if 'blockid_hov' in DICT :
605            self.blockid_hov = DICT['blockid_hov']
606
607    def set_warn_level(self, DICT):
608        if 'warn_level' in DICT :
609            self.warn_level = DICT['warn_level']
610
611    # [PT: May 16, 2019] updated to deal with parsing of PBAR stuff here
612    def add_text(self, DICT):
613        if 'text' in DICT :
614            xx = parse_apqc_text_fields(DICT['text'])
615            self.text+= xx
616#            if type(DICT['text']) == list :
617#                xx = '\n'.join(DICT['text'])
618#                self.text+= xx
619#            else:
620#                self.text+= DICT['text']
621
622    # [PT: May 16, 2019] updated to deal with parsing of PBAR stuff here
623    def add_subtext(self, DICT):
624        if 'subtext' in DICT :
625            xx = parse_apqc_text_fields(DICT['subtext'])
626            self.subtext+= xx
627#            if type(DICT['subtext']) == list :
628#                xx = '\n'.join(DICT['subtext'])
629#                self.subtext+= xx
630#            else:
631#                self.subtext+= DICT['subtext']
632
633    # this just runs through all possible things above and fills in
634    # what it can
635    def set_all_from_dict(self, DICT):
636        self.set_title(DICT)
637        self.set_itemtype(DICT)
638        self.set_itemid(DICT)
639        self.set_blockid(DICT)
640        self.set_blockid_hov(DICT)
641        self.add_text(DICT)
642        self.add_subtext(DICT)
643        self.set_warn_level(DICT)
644
645# -------------------------------------------------------------------
646
647# these are the properties/fields that the incoming *page-title* info
648# might have.  These define what fields the jsons created by
649# @ss_review_html (and therefore, ultimately in lib_apqc_tcsh.py) can
650# have.
651class apqc_title_info:
652
653    title       = ""
654    subj        = ""
655    taskname    = ""
656    itemtype    = ""
657    itemid      = ""
658    blockid     = ""
659    blockid_hov = ""
660
661    def set_title(self, DICT):
662        if 'title' in DICT :
663            self.title = DICT['title']
664
665    def set_itemtype(self, DICT):
666        if 'itemtype' in DICT :
667            self.itemtype = DICT['itemtype']
668
669    def set_itemid(self, DICT):
670        if 'itemid' in DICT :
671            self.itemid = DICT['itemid']
672
673    def set_blockid(self, DICT):
674        if 'blockid' in DICT :
675            self.blockid = DICT['blockid']
676
677    def set_blockid_hov(self, DICT):
678        if 'blockid_hov' in DICT :
679            self.blockid_hov = DICT['blockid_hov']
680
681    def set_taskname(self, DICT):
682        if 'taskname' in DICT :
683            self.taskname = DICT['taskname']
684
685    def set_subj(self, DICT):
686        if 'subj' in DICT :
687            self.subj = DICT['subj']
688
689    # this just runs through all possible things above and fills in
690    # what it can
691    def set_all_from_dict(self, DICT):
692        self.set_title(DICT)
693        self.set_itemtype(DICT)
694        self.set_itemid(DICT)
695        self.set_blockid(DICT)
696        self.set_blockid_hov(DICT)
697        self.set_taskname(DICT)
698        self.set_subj(DICT)
699
700# -------------------------------------------------------------------
701
702def write_json_file( ll, fname ):
703
704    olist = []
705    olist.append( json_cols )
706
707    # skip the first element here because it came from the title, and
708    # that doesn't have a QC button with it (it's just the 'Top' of
709    # the page).
710    for i in range(1,len(ll)):
711        x = ll[i]
712        olist.append( [x[0], "", ""] )
713
714    # output with indentation
715    ojson = json.dumps( olist, indent=4 )
716    fff = open( fname, "w" )
717    fff.write( ojson )
718    fff.close()
719
720# --------------------------------------------------------------------
721
722# !!! UNUSED
723#def wrap_block_lab(x, vpad=0):
724#    y = """<h3><center>block: """+x+"""</center></h3>"""
725#    if vpad:
726#        y= """\n"""+y
727#        y+="""\n"""
728#    return y
729
730# -------------------------------------------------------------------
731
732def make_nav_table(llinks, max_wlevel=''):
733    # table form, not ul
734    N = len(llinks)
735    idx = 0
736
737    # =======================================================================
738    # dummy, background nav
739
740    y = '''
741    <div class="navbar">
742      <table style="width: 100%">
743
744        <tr>
745          <td style="width: 100%">
746            <a style="text-align: left"> {0} </a>
747          </td>
748        </tr>
749
750        <tr>
751          <td style="width: 100%">
752            <button class="button-generic button-LHS btn0" onclick="">
753            {0} </button>
754          </td>
755        </tr>
756
757      </table>
758    </div>
759    '''.format( NULL_BTN0 )
760
761    # =======================================================================
762    # real, foreground nav
763
764    y+= '''\n<div class="navbar">\n'''
765
766    # -----------------------------------------------------
767    # L-floating part: section anchors and rating buttons
768    # NB: these are fixed width
769
770    ## note about keycodes on internet explorer, might have to do
771    ## something like this for each one:
772    # https://stackoverflow.com/questions/1750223/javascript-keycode-values-are-undefined-in-internet-explorer-8
773
774    for i in range(0, N):
775        ll, hov = llinks[i][0], llinks[i][1]
776        #print(ll)
777
778        color_change = ''
779
780        # Put lines around "FINAL" element
781        if i<N-1 :
782            finaltab = ''
783            if ll == 'warns' and max_wlevel :
784                wcol = lahc.wlevel_colors[max_wlevel]
785                if lahc.wlevel_ranks[max_wlevel] > lahc.wlevel_ranks['mild'] :
786                    finaltab = '''style="color: {}; '''.format("#000")
787                    finaltab+= '''background-color: {};" '''.format(wcol)
788                else:
789                    finaltab = '''style="color: {};" '''.format(wcol)
790
791        else:
792            finaltab = '''style="background-color: #ccc; color: #000;" '''
793
794        # new table
795        y+= '''<table style="float: left">\n'''
796
797        # TOP ROW (blockid)
798        y+= '''
799        <tr>
800          <td class="td1" id=td1_{0}>
801            <button class="button-generic button-LHS btn5" id="btn5_{0}"
802            onmousedown="moveToDiv(hr_{0})" title="{1}" {2}
803            onkeypress="if ( event.keyCode == 13 ) {{ moveToDiv(hr_{0}); }}">
804            {0}</button>
805          </td>
806        </tr>
807        '''.format( ll, hov, finaltab )
808
809        # BOT ROW (QC button)
810        y+= '''<td >''' # set boundary between QC buttons here
811        if i :
812            # NB: with button clicks, if using onkeypress with
813            # onclick, the former *also* drives the latter a second
814            # time, so get annoying behavior; hence, distinguish those
815            y+= '''
816              <button class="button-generic button-LHS btn1" id="btn1_{0}" data-txtcomm=""
817              onmousedown="btn1Clicked(event, this)"
818              onkeypress="if ( event.keyCode == 13 ) {{ btn1Clicked(event, this); }}"
819              {1}</button>
820            </td>
821            '''.format( ll, NULL_BTN1 )
822        else:
823            y+= '''
824              <button class="button-generic button-LHS btn0" id="btn0_{0}"
825              onclick=""
826              title="{1}">
827              {2}</button></td>
828            '''.format( ll, brate_hover, "FORM:" )
829        y+= '''</tr>\n'''
830        y+= '''</table>'''
831
832        if i :
833            # ~dropdown form button
834            ## NB: the onkeydown stuff makes it that hitting "Enter"
835            ## (event.keyCode == 10 || event.keyCode == 13) inside the
836            ## text field is like submitting the text (and the
837            ## .preventDefault() means that it does NOT input a
838            ## newline):
839            ## https://stackoverflow.com/questions/155188/trigger-a-button-click-with-javascript-on-the-enter-key-in-a-text-box
840            ## https://stackoverflow.com/questions/26975349/textarea-wont-stop-making-new-line-when-enter-is-pressed
841            ## ... and hitting "Esc" (event.keyCode == 27) is like
842            ## canceling.
843            y+= '''
844            <div class="form-popup" id="cform_{0}" >
845                <form class="form-container" onsubmit="return false;">
846                <textarea type="text" placeholder="Enter comment"
847                rows="4" cols="40" id="comm_{0}"
848                onkeydown="if (event.keyCode == 10 || event.keyCode == 13) {{
849                   event.preventDefault(); keepFromCommentForm(comm_{0}.id, cform_{0}.id);}}
850                   else if (event.keyCode == 27) {{
851                       clearCommentForm(comm_{0}.id, cform_{0}.id); }}">
852                </textarea>
853                <button type="button" class="btn"
854                onclick="keepFromCommentForm(comm_{0}.id, cform_{0}.id)">keep+close</button>
855                <button type="button" class="btn cancel"
856                onclick="clearCommentForm(comm_{0}.id, cform_{0}.id)">clear+close</button>
857                </form>
858            </div>
859            '''.format( ll )
860
861    # ------------------------------------------------------
862    # R-floating part: subj ID and SAVE button
863    # NB: this is flexible width
864    bsave  = 'SAVE'
865    bhelp  = 'HELP'
866    bgood  = 'A+'  ; bgood_ind  =  1
867    bbad   = 'Ax'  ; bbad_ind   =  2
868    bother = 'A?'  ; bother_ind =  0
869    bclear = 'clr' ; bclear_ind = -1
870
871    # Start right-side table
872    y+= '''<table style="float: right; margin-right: 2px">\n'''
873
874    # ROW: "all fill" buttons-- click (or Enter) fills empty QC buttons,
875    # dblclick (or ctrl+Enter) fills ALL QC buttons
876    y+= '''<tr>\n'''
877    y+= '''<td style="width: 180px; white-space:nowrap;">\n'''
878
879    y+= '''
880<button class="button-generic button-RHS button-RHS-little btn2{0}" title="{1}"
881onmousedown="allYourBaseAreBelongToUs({2})"
882onkeydown="if (event.keyCode == 10 || event.keyCode == 13) {{ if (event.ctrlKey) {{
883reallyAllYourBaseAreBelongToUs({2}); }} else {{ allYourBaseAreBelongToUs({2}); }} }} "
884ondblclick="reallyAllYourBaseAreBelongToUs({2})">
885{3}</button>
886    '''.format( 'good', bgood_hover, bgood_ind, bgood )
887
888    y+= '''
889<button class="button-generic button-RHS button-RHS-little btn2{0}" title="{1}"
890onmousedown="allYourBaseAreBelongToUs({2})"
891onkeydown="if (event.keyCode == 10 || event.keyCode == 13) {{ if (event.ctrlKey) {{
892reallyAllYourBaseAreBelongToUs({2}); }} else {{ allYourBaseAreBelongToUs({2}); }} }} "
893ondblclick="reallyAllYourBaseAreBelongToUs({2})">
894{3}</button>
895    '''.format( 'bad', bbad_hover, bbad_ind, bbad )
896
897    y+= '''
898<button class="button-generic button-RHS button-RHS-little btn2{0}" title="{1}"
899onmousedown="allYourBaseAreBelongToUs({2})"
900onkeydown="if (event.keyCode == 10 || event.keyCode == 13) {{ if (event.ctrlKey) {{
901reallyAllYourBaseAreBelongToUs({2}); }} else {{ allYourBaseAreBelongToUs({2}); }} }} "
902ondblclick="reallyAllYourBaseAreBelongToUs({2})">
903{3}</button>
904    '''.format( 'other', bother_hover, bother_ind, bother )
905
906    y+= '''
907<button class="button-generic button-RHS button-RHS-little btn2{0}" title="{1}"
908onkeydown="if (event.keyCode == 10 || event.keyCode == 13) {{ if (event.ctrlKey) {{
909reallyAllYourBaseAreBelongToUs({2}); }} }} "
910ondblclick="reallyAllYourBaseAreBelongToUs({2})">
911{3}</button>
912    '''.format( 'clear', bclear_hover, bclear_ind, bclear )
913
914    y+= '''</td>\n'''
915    y+= '''</tr>\n'''
916
917    # ROW:  hyperlinks (anchors) within the page
918    y+= '''<tr>\n'''
919    y+= '''<td style="width: 180px; white-space:nowrap;" id=td3_{}>'''.format( bsave )
920
921    y+= '''<button class="button-generic button-RHS btn3save" title="{}" '''.format( bsave_hover )
922    y+= '''onclick="doSaveAllInfo()">'''
923    y+= '''{}</button>\n'''.format( bsave )
924
925    y+= '''<button class="button-generic button-RHS btn3help" title="{}" '''.format( bhelp_hover )
926    y+= '''onclick="doShowHelp()">'''
927#    y+= '''href="help.html" target="_blank">'''
928#    y+= '''onclick="location.href='help.html';">'''
929    y+= '''{}</button>\n'''.format( bhelp )
930
931    y+= '''</td>\n'''
932    y+= '''</tr>\n'''
933
934    # End right-side table
935    y+= '''</table>'''
936    y+= '''</div>'''
937
938    return y
939
940# -------------------------------------------------------------------
941# -------------------------------------------------------------------
942# -------------------------------------------------------------------
943
944# JAVASCRIPT script functions and variables
945
946def make_javascript_btn_func(subj ):
947
948    y = ''
949
950    y+= '''<script type="text/javascript">\n'''
951
952    y+= '''
953
954// global vars
955var allBtn1, allTd1, allhr_sec;  // selection/location IDs
956var topi, previ;                 // keeps our current/last location
957var jsonfile = "apqc_{0}.json";  // json file: apqc_SUBJ.json
958var qcjson;                      // I/O json of QC button responses
959var nb_offset = 66-1;            // navbar height: needed for scroll/jump
960
961// properties of QC buttons that get toggled through
962const bkgds   = [ "#fff" , "#67a9cf", "#d7191c"  ];
963const valeurs = [ "?"    , "+"      , "X"        ];
964const tcols   = [ "#777" , "#FFF"   , "#000"     ];
965
966'''.format( subj )
967
968    # --------------- load/reload initialize -----------------
969
970    # gets run at loadtime
971    y+= '''
972//window.onload = function() {
973function RunAtStart() {
974
975    // initialize arrays and location
976    initializeParams();
977
978    // read in JSON
979    loadJSON(jsonfile, function(unparsed_json) {
980      // give the global variable values
981      window.qcjson = JSON.parse(unparsed_json);
982    });
983
984    CheckJsonfileMatchesQcbuttons();
985
986    ApplyJsonfileToQcbuttons();
987};
988
989'''
990
991    # OFF AT HTE MOMENT, but a guard for reloading page
992    y+= '''
993    window.onbeforeunload = function(event)
994    {
995        return confirm();
996    };
997'''
998
999    y+= '''
1000// This function gets run when page loads ("onload").
1001function initializeParams() {
1002    allBtn1   = document.getElementsByClassName("btn1");   // btn1_vepi, btn1_*
1003    allTd1    = document.getElementsByClassName("td1");    // td1_vepi,  td1_*
1004    allhr_sec = document.getElementsByClassName("hr_sec"); // hr_vepi,   hr_*
1005
1006    topi      = findTopSectionIdx()       // idx of current loc
1007    previ     = topi;                     // init "previous" idx
1008    setTd1Border(topi, "#ffea00"); //"yellow"); // show location
1009}
1010'''
1011
1012    # read in JSON file, from:
1013    # https://codepen.io/KryptoniteDove/post/load-json-file-locally-using-pure-javascript
1014    # https://stackoverflow.com/questions/7346563/loading-local-json-file
1015    # importantly, the xobjs.open(...) func NEEDS to have the 'false'
1016    # set to read synchronously, which is necessary for the JSON to
1017    # load fully before use-- slower, but this is a small file
1018    y+= '''
1019function loadJSON(ifile, callback) {
1020  var xobj = new XMLHttpRequest();
1021  xobj.overrideMimeType("application/json");
1022  xobj.open('GET', ifile, false);  // need 'false' for synchrony!
1023  xobj.onreadystatechange = function () {
1024    if (xobj.readyState == 4 && xobj.status == "200") { !!!!
1025      callback(xobj.responseText);
1026    }
1027  };
1028  xobj.send(null);
1029}
1030'''
1031
1032    # Both the QC element names AND their order need to match
1033    y+= '''
1034function CheckJsonfileMatchesQcbuttons() {
1035    var Nele = qcjson.length;
1036
1037    for( var i=1; i<Nele; i++ ) {
1038        var ele = qcjson[i][0];
1039
1040        var jj = i - 1;  // offset because of col heading in JSON
1041        var bname = new String(allBtn1[jj].id);
1042        var bpost = bname.slice(5);  // skip the 'btn1_' part of button ID
1043
1044        if ( bpost != ele ) {
1045             window.alert("**Error: postfix on button ID " + bname + " does not match with JSON entry " + ele);
1046             throw "DONE";
1047        }
1048    }
1049}
1050'''
1051
1052    # Because order matches (offset by 1), we can just apply directly
1053    # with the counting index, based on the allBtn1 list.
1054    y+= '''
1055// This function gets run when page loads ("onload").
1056function ApplyJsonfileToQcbuttons() {
1057    var Nele = qcjson.length;
1058
1059    for( var i=1; i<Nele; i++ ) {
1060
1061        var jj = i - 1;  // offset because of col heading in JSON
1062        var bid = new String(allBtn1[jj].id);
1063
1064        // Set the comments first, because the button presentation
1065        // of rating depends on whether that has been set
1066        sendCommentToButtonAndForm(qcjson[i][2], bid);
1067        sendRatingToButton(qcjson[i][1], bid);
1068    }
1069
1070}
1071'''
1072
1073    # This function gets run when page loads ("onload").  'ss' is the
1074    # JSON rating, and 'bid' is the button ID in AllBtn1.
1075    y+= '''
1076function sendRatingToButton(ss, bid) {
1077
1078    if ( ss == "good" ) {
1079       setThisButtonRating(bid, 1);
1080    } else if ( ss == "bad" ) {
1081       setThisButtonRating(bid, 2);
1082    } else if ( ss == "other" ) {
1083       setThisButtonRating(bid, 0);
1084    } else if ( ss == "null" || ss == "" ) {
1085       setThisButtonRating(bid, -1);
1086    } else {
1087      window.alert("**Error: unallowed rating in JSON:" + ss);
1088      throw "DONE";
1089    }
1090}
1091'''
1092
1093    # When the JSON is read in, get comments and give any text to both
1094    # the btn1 and associated comment form textarea
1095    y+= '''
1096function sendCommentToButtonAndForm(comm, bid) {
1097    thisButtonGetsAComment(bid, comm);
1098
1099    var bname = new String(bid); // basename
1100    var cid   = 'comm_' + bname.slice(5);  // skip the 'btn1_' part of button ID
1101
1102    // because of how "null" is read in; this just matters in form
1103    if ( comm == "null" ) {
1104        var comm = "";
1105    }
1106    thisFormTextAreaGetsAComment(cid, comm);
1107}
1108'''
1109
1110    # --------------- scroll location in page stuff -----------------
1111
1112    # Checks/rechecks whenever change in page location occurs.
1113    y+= '''
1114window.addEventListener("scroll", function(event) {
1115
1116    var newi = findTopSectionIdx();
1117
1118    if ( newi != topi ) {
1119        setTd1Border(newi, "#ffea00"); //"yellow");
1120        setTd1Border(topi,  "inherit");      ; //"#FFF", "#444");
1121        previ = topi;
1122        topi  = newi;
1123    }
1124}, false);
1125'''
1126
1127   # Just go through (short) list from top, and first one that has pos
1128   # coor, is the one at top
1129    y+= '''
1130function findTopSectionIdx() {
1131    for( var i=0; i<allhr_sec.length; i++ ) {
1132        var bid = allhr_sec[i].id;
1133        var bbox = document.getElementById(bid).getBoundingClientRect();
1134        if ( bbox.top - nb_offset > 1 ) {
1135            break;
1136        }
1137    }
1138    return i-1;
1139}
1140'''
1141
1142    y+= '''
1143function setTd1Border(ii, bkgdcol) {
1144    var newtid = allTd1[ii].id;
1145    document.getElementById(newtid).style.background = bkgdcol;
1146}
1147'''
1148
1149    # --------------- QC button: toggle indiv or fill group -------------
1150
1151    # click on the QC buttons will scroll through the color values
1152    ## ctrl+click on the QC buttons will toggle between the comment
1153    ## form being open or closed (saving what is in form when closing).
1154    y+= '''
1155function btn1Clicked(event, button) {
1156    if (event.ctrlKey) {
1157       btn1ClickedWithCtrl(event, button);
1158    }  else {
1159       changeColor(button); //alert("The CTRL key was NOT pressed!");
1160    }
1161
1162}
1163'''
1164
1165    y+= '''
1166function btn1ClickedWithCtrl(event, button) {
1167    // get the form ID from button ID
1168    var bname = new String(button.id);
1169    var bpost = bname.slice(5); // skip the 'btn1_' part of button ID
1170    var cFormID = 'cform_' + bpost;
1171    // if closed, this opens it; otherwise, it closes it
1172    if ( document.getElementById(cFormID).style.display == false ||
1173         document.getElementById(cFormID).style.display == "none" ) {
1174         openCommentForm(cFormID, button.id);
1175    } else {
1176         keepFromCommentFormViaBtn1(button.id, cFormID);
1177    }
1178}
1179'''
1180
1181
1182    # Toggle individual;
1183    ## ... and VERY useful comment about the "!important" keyword for
1184    ## hovering after changing DOM properties.
1185    ## https://stackoverflow.com/questions/46553405/css-hover-not-working-after-javascript-dom
1186    y+= '''
1187function changeColor(button) {
1188  newidx = Number(button.dataset.idx || 0);    // idx=0 on first click
1189  newidx = (newidx + 1) % bkgds.length;        // calc new idx, mod Ncol
1190  button.dataset.idx       = newidx;            // store new idx in ele
1191  button.style.color       = tcols[newidx];     // set color
1192  button.style.background  = bkgds[newidx];     // set bkgd
1193  button.style.borderColor = bkgds[newidx];     // set bkgd
1194  button.textContent       = valeurs[newidx];
1195  checkIfButtonCommented( button );            // set text
1196}
1197'''
1198
1199    y+= '''
1200function checkIfButtonCommented( button ) {
1201
1202    var value = button.textContent;
1203    var bcomm = button.dataset.txtcomm;
1204    var VAL_HAS_QUOTE = value.includes(`"`);
1205
1206    // if no comment, make sure there is no
1207    if ( ( bcomm == "" || bcomm == "null" ) ) {
1208       if ( VAL_HAS_QUOTE ) {
1209         var newval = value.replace(/\"/g, "");
1210         button.textContent = newval;
1211       }
1212    } else {
1213       if ( !VAL_HAS_QUOTE ) {
1214          button.textContent = `"` + value + `"`;
1215       }
1216    }
1217}
1218'''
1219
1220    # two arguments: the button ID 'bid' from an element of
1221    # AllBt1n, and the 'idx' which picks out valeurs[idx]
1222    # etc. properties.
1223    y+= '''
1224function setThisButtonRating(bid, idx) {{
1225    // normal values
1226    if ( idx >= 0 ) {{
1227      document.getElementById(bid).textContent      = valeurs[idx];
1228      document.getElementById(bid).style.background = bkgds[idx];
1229      document.getElementById(bid).style.borderColor = bkgds[idx];
1230      document.getElementById(bid).style.color      = tcols[idx];
1231      document.getElementById(bid).dataset.idx      = idx;
1232      checkIfButtonCommented( document.getElementById(bid) );
1233
1234    }} else {{
1235    // the reset, for "null" JSON
1236      document.getElementById(bid).textContent      = "{}";
1237      document.getElementById(bid).style.background = ''; // reset to CSS
1238      document.getElementById(bid).style.borderColor = ''; // reset to CSS
1239      document.getElementById(bid).style.color      = ''; // reset to CSS
1240      document.getElementById(bid).dataset.idx      = 0; //null;
1241    }}
1242}}
1243'''.format ( NULL_BTN1 )
1244
1245    y+= '''
1246function isBtn1InNullState( bid ) {{
1247    var tc = document.getElementById(bid).textContent;
1248    if ( tc == "{}" ) {{
1249        return true;
1250    }} else {{
1251        return false;
1252    }}
1253}}
1254'''.format ( NULL_BTN1 )
1255
1256    # two arguments: the button ID 'bid' from an element of AllBt1n,
1257    # and the 'comment' that gets added/overwritten (in the newly
1258    # created element, txtcomm).  Basically used to put the form
1259    # comments into the button fields, and then later into jsons.
1260    y+= '''
1261function thisButtonGetsAComment(bid, comm) {
1262    document.getElementById(bid).dataset.txtcomm = comm;
1263
1264    // and don't allow a null state anymore if it has a comment:
1265    // update it to "other"/"?"
1266    if ( comm == "" || comm == "null" ) {
1267    } else {
1268       if ( isBtn1InNullState(bid) ) {
1269           setThisButtonRating(bid, 0);
1270       }
1271    }
1272
1273    // and reset quotes, if necessary.
1274    //window.alert(bid);
1275    checkIfButtonCommented( document.getElementById(bid) );
1276}
1277'''
1278
1279    # "ALL OTHER" fill button, here to set every btn1-button value to
1280    # "+" or "x", depending on input arg 'ii' (index in list)
1281    y+= '''
1282function allYourBaseAreBelongToUs(ii) {{
1283   for( var i=0; i<allBtn1.length; i++ ) {{
1284     var bid = allBtn1[i].id;
1285     var ival = document.getElementById(bid).textContent;
1286     if ( ival == "{}" ) {{
1287       setThisButtonRating(bid, ii);
1288     }}
1289   }}
1290}}
1291'''.format( NULL_BTN1 )
1292
1293    # "ALL-ALL" fill button: regardless of initial state set every
1294    # btn1-button value to "+" or "x", depending on input arg 'ii'
1295    # (index in list); that is, this overruns earlier button values
1296    y+= '''
1297function reallyAllYourBaseAreBelongToUs(ii) {
1298   for( var i=0; i<allBtn1.length; i++ ) {
1299     var bid = allBtn1[i].id;
1300     var ival = document.getElementById(bid).textContent;
1301     setThisButtonRating(bid, ii);
1302     if ( ii < 0 ) {
1303        sendCommentToButtonAndForm("", bid);
1304     }
1305   }
1306}
1307'''
1308
1309    # ------------------- commentize form ------------------------
1310
1311    # Get position coordinates of an object, knowing its ID
1312    y+= '''
1313function getBoundingRect(iid) {
1314    var bbox = document.getElementById(iid).getBoundingClientRect();
1315    return bbox;
1316}
1317'''
1318
1319    # Use this to place the thing: the height comes from the height of
1320    # the menu bar, and the L-R positioning comes from the QC button
1321    # itself.
1322    y+= '''
1323function openCommentForm(cfID, bid) {
1324    document.getElementById(cfID).style.display = "block";
1325    var bbox = getBoundingRect(bid);
1326    document.getElementById(cfID).style.left  = bbox.left;
1327}
1328'''
1329
1330    # just close the form button when done (mainly for ctrl+click)
1331    y+= '''
1332function closeCommentForm(cfID) {
1333    document.getElementById(cfID).style.display = "none";
1334}
1335'''
1336
1337    # close *and* remove value (esc key, or clear+close button)
1338    y+= '''
1339function clearCommentForm(cid, cfID) {
1340    document.getElementById(cid).value = "";
1341
1342    // get the btn1 ID from comm ID
1343    var bname = new String(cid); // basename
1344    var bid   = "btn1_" + bname.slice(5);  // skip the 'comm_' part of button ID
1345
1346    thisButtonGetsAComment(bid, null);
1347
1348    closeCommentForm(cfID);
1349}
1350'''
1351
1352    # needed for when JSON file is read in, to give values from that
1353    # to the text area field (as well as bt1n)
1354    y+= '''
1355function thisFormTextAreaGetsAComment(cid, comm) {
1356    document.getElementById(cid).value = comm;
1357}
1358'''
1359
1360    # "Saving" here means taking the comment (cid) and associating it
1361    # with a button (bid), while also closing the comment form (cfID).
1362    # (enter key, or keep+close button)
1363    y+= '''
1364function keepFromCommentForm(cid, cfID) {
1365
1366    // user's text
1367    var commtext = document.getElementById(cid).value;
1368
1369    // get the btn1 ID from comm ID
1370    var bname = new String(cid); // basename
1371    var bid   = "btn1_" + bname.slice(5);  // skip the 'comm_' part of button ID
1372
1373    thisButtonGetsAComment(bid, commtext);
1374    closeCommentForm(cfID);
1375}
1376'''
1377
1378    # Same as keepFromCommentForm(...), but used when user is
1379    # ctrl+clicking on btn1 to close comment
1380    y+= '''
1381function keepFromCommentFormViaBtn1(bid, cfID) {
1382
1383    // get the btn1 ID from comm ID
1384    var bname = new String(bid); // basename
1385    var cid   = 'comm_' + bname.slice(5);  // skip the 'comm_' part of button ID
1386
1387    // user's text
1388    var commtext = document.getElementById(cid).value;
1389
1390    thisButtonGetsAComment(bid, commtext);
1391    closeCommentForm(cfID);
1392}
1393'''
1394
1395    # ------------------- page scrolling ------------------------------
1396
1397    # THIS is now how we move on the page, so that there is no need to
1398    # jump into the page, and hence tabbing through buttons is allowed.
1399    y+= '''
1400function moveToDiv( hr_sec ) {
1401    var sid = new String(hr_sec.id)
1402    var rect = getBoundingRect(sid);
1403
1404    var scrtop =  this.scrollY;
1405    var newloc = rect.top + scrtop - nb_offset;
1406    window.scrollTo(0, newloc);
1407
1408    //window.alert("earlier: " + rect.top + ", and now: " + this.scrollY);
1409}
1410'''
1411
1412
1413    # ------------------- saving into JSON obj ------------------------
1414
1415    # submit values by element and col names
1416    y+= '''
1417function saveJsonValuesByNames(elename, colname, VAL) {
1418
1419    cc = findCol(colname);
1420    rr = findQceleRow(elename);
1421
1422    qcjson[rr][cc] = VAL;
1423}
1424'''
1425
1426    # submit values by row and col nums
1427    y+= '''
1428function saveJsonValuesByNums(rr, cc, VAL) {
1429    Ncol = qcjson[0].length;
1430    if ( cc >= Ncol ) {
1431      window.alert("**Error: Column [" + cc + "] not in JSON table!");
1432      throw "DONE";
1433    }
1434
1435    var Nrow = qcjson.length;
1436    if ( i >= Nrow ) {
1437      window.alert("**Error: QC element [" + rr + "] not in JSON table!");
1438      throw "DONE!";
1439    }
1440
1441    qcjson[rr][cc] = VAL;
1442}
1443'''
1444
1445    # find row index of QC element in JSON table
1446    y+= '''
1447function findQceleRow(elename) {
1448    var Nrow = qcjson.length;
1449    for( var i=1 ; i<Nrow ; i++ ) {
1450       if ( elename == qcjson[i][0] ) {
1451         break;
1452       }
1453    }
1454
1455    if ( i >= Nrow ) {
1456      window.alert("**Error: QC element " + elename + " not in JSON table!");
1457      throw "DONE";
1458    }
1459
1460    return i;
1461}
1462'''
1463
1464    # find col index of item in JSON table
1465    y+= '''
1466function findCol(colname) {
1467    Ncol = qcjson[0].length;
1468    for( var cc=1 ; cc<Ncol ; cc++ ) {
1469       if ( colname == qcjson[0][cc] ) {
1470         break;
1471       }
1472    }
1473
1474    if ( cc >= Ncol ) {
1475       window.alert("**Error: Column " + colname + " not in JSON table!");
1476       throw "DONE";
1477    }
1478
1479    return cc;
1480}
1481'''
1482
1483
1484
1485    # At present, THIS is the form of the input json: just a list of
1486    # lists.  This will be convenient in order to remain ordered
1487    # (dictionaries are *not* ordered).  Column headings are still
1488    # included, for the moment.  At the moment, the column heading
1489    # names are hardcoded into the data I/O.
1490
1491    '''
1492[
1493    ["qcele", "rating", "comment"],
1494    ["vepi", "good", "null"],
1495    ["ve2a", "bad",  "hello"],
1496    ["va2t", "null", "null"],
1497    ["vstat", "null", "null"],
1498    ["mot6", "null", "hello"],
1499    ["motE", "null", "null"],
1500    ["out", "null", "hello"],
1501    ["regps", "null", "null"],
1502    ["regcs", "null", "hello"],
1503    ["warns", "null", "null"],
1504    ["summ", "null", "hello"]
1505]
1506'''
1507
1508    # ----------- SAVE FORM: update JSON (qcjson) and save to file
1509    # ----------- (hopefully)!
1510
1511    # The Saver
1512    y+= '''
1513function doSaveAllInfo() {
1514    updateLocalJson();
1515
1516    var text     = JSON.stringify(qcjson);
1517    //var filename = "apqc.json";
1518    saveDownloadJsonfile(text, jsonfile);
1519
1520}
1521'''
1522    # The Helper
1523    y+= '''
1524function doShowHelp() {
1525    window.open('help.html', '_blank');
1526
1527}
1528'''
1529
1530    # Step 1 of saving the dataset: push button vals to JSON
1531    y+= '''
1532function updateLocalJson() {
1533    var Nele = qcjson.length;
1534    for( var i=1; i<Nele; i++ ) {
1535
1536        var qcele = qcjson[i][0];
1537
1538        var jj    = i - 1;  // offset because of col heading in JSON
1539        var bid   = new String(allBtn1[jj].id);
1540
1541        var rattext = translateBtn1TextToJsonRating(document.getElementById(bid).textContent);
1542        saveJsonValuesByNames(qcele, "rating", rattext);
1543
1544        // save the comment part
1545        var commtext = document.getElementById(bid).dataset.txtcomm;
1546        saveJsonValuesByNames(qcele, "comment", commtext);
1547    }
1548    // window.alert("SAVING JSON: " + qcjson);
1549}
1550'''
1551
1552    # Step 2 of saving the dataset: write to file
1553    y+= '''
1554function saveDownloadJsonfile(text, filename){
1555    var a = document.createElement('a');
1556    a.setAttribute('href', 'data:text/plain;charset=utf-u,'+encodeURIComponent(text));
1557    a.setAttribute('download', filename);
1558    document.body.appendChild(a);
1559    a.click();
1560    document.body.removeChild(a);
1561}
1562'''
1563
1564    y+= '''
1565function translateBtn1TextToJsonRating( tt ) {
1566    if ( tt.includes("+") ) {
1567       return "good";
1568    } else if ( tt.includes("X") ) {
1569       return "bad";
1570    } else if ( tt.includes("?") ) {
1571       return "other";
1572    } else if ( tt == "null" || tt == "" ) {
1573       return "null";
1574    } else {
1575      window.alert("**Error: unallowed text/rating in button:" + tt);
1576      throw "DONE";
1577    }
1578}
1579'''
1580
1581    y+= '''
1582</script>
1583'''
1584
1585    return y
1586
1587# -------------------------------------------------------------------
1588# -------------------------------------------------------------------
1589# -------------------------------------------------------------------
1590# -------------------------------------------------------------------
1591# -------------------------------------------------------------------
1592# -------------------------------------------------------------------
1593
1594def wrap_page_title( xtitle, xstudy, xsubj,
1595                     vpad=0, addclass="", blockid='', padmarg=0 ):
1596
1597    # start the first div on the page
1598    y = '''<div class="div_pad_class">'''
1599
1600    # the boundary line: location+ID necessary for highlighting page
1601    # location
1602    y+= '''\n\n<hr class="hr_sec" id="hr_{}"/>'''.format(blockid)
1603
1604    # the title
1605    y+= '''<div id="{}" '''.format(blockid)
1606
1607    # this line offsets the anchor location for the navigation bar to
1608    # head to: the values here should be equal to the height of the
1609    # navigation bar (plus the line beneath it).
1610    y+= ''' style="padding-top: {0}px; margin-top: -{0}px;">'''.format(padmarg)
1611
1612    y+= '''
1613    <h1><center> {} <center></h1></div>
1614
1615<div style="text-align: center;">
1616    <div style="display: inline-block; text-align: left;">
1617    <pre><h2>subj: {}</h2></pre>
1618    <pre><h3>task: {}</h3></pre>
1619
1620    </div>
1621</div>
1622'''.format( xtitle, xsubj, xstudy )
1623
1624
1625
1626    if vpad:
1627        y = """\n"""+y
1628        y+="""\n"""
1629
1630    return y
1631
1632
1633# -------------------------------------------------------------------
1634
1635def wrap_block_title(x, vpad=0, addclass="", blockid='', padmarg=0):
1636
1637    # close the previous section div (at least the title will have one)
1638    y = '''</div>\n\n'''
1639
1640    # start the new section div
1641    y+= '''<div class="div_pad_class">'''
1642
1643    # the boundary line: location+ID necessary for highlighting page
1644    # location
1645    y+= '''\n\n<hr class="hr_sec" '''
1646    if blockid :
1647        y+= ''' id="hr_{}" '''.format(blockid)
1648    y+= '''/>\n'''
1649
1650    # the title
1651    y+= '''<div '''
1652    if blockid :
1653        y+= ''' id="{}" '''.format(blockid)
1654    # this line offsets the anchor location for the navigation bar to
1655    # head to: the values here should be equal to the height of the
1656    # navigation bar (plus the line beneath it).
1657    y+= ''' style="padding-top: {0}px; margin-top: -{0}px;"'''.format(padmarg)
1658    y+= """><pre """
1659    y+= ''' {} '''.format(addclass)
1660    y+= """><center>["""+blockid+"""]<b> """
1661    y+= """<u>"""+x+"""</u>"""
1662    y+= ' '*(len(blockid)+3)       # balance blockid text
1663    y+= """</b></center></pre></div>"""
1664    if vpad:
1665        y= """\n"""+y
1666        y+="""\n"""
1667    return y
1668
1669# -------------------------------------------------------------------
1670
1671def wrap_block_text( x, vpad=0, addclass="", dobold=True, itemid='',
1672                     padmarg=0 ):
1673    addid = ''
1674    if itemid :
1675        addid = ''' id="{}" '''.format( itemid )
1676
1677    y = """<div {0}""".format( addid )
1678    y+= ''' style="padding-top: {0}px; margin-top: -{0}px;"'''.format(padmarg)
1679    y+= ''' {} >'''.format(addclass)
1680    if dobold :
1681        y+= """<pre><b>"""+x+"""</b></pre></div>"""
1682    else:
1683        y+= """<pre>"""+x+"""</pre></div>"""
1684    if vpad:
1685        y= """\n"""+y
1686        y+="""\n"""
1687    return y
1688
1689# -------------------------------------------------------------------
1690
1691def wrap_img(x, wid=500, vpad=0, addclass=""):
1692    # [PT: Nov 20, 2018] needed this next line to center the text, and
1693    # needed "display: inline-block" in the img {} def to not have
1694    # whole line be a link.
1695
1696    y = ''
1697    y+= vpad*'\n'
1698
1699    y+= '''<div style="text-align: center">
1700    <a href="{0}"><img src="{0}" alt="{0}" {1}
1701    style="display: inline-block; text-align: center;"></a>
1702    </div>'''.format( x, addclass)
1703    y+= vpad*'\n'
1704
1705    return y
1706
1707# -------------------------------------------------------------------
1708
1709# string literal
1710def wrap_dat(x, wid=500, vpad=0, addclass="", warn_level = "",
1711             remove_top_empty=False):
1712
1713    # some formatting: get rid of unnecessary top empty lines
1714    if remove_top_empty:
1715        newx = x.split("\n")
1716        if not(newx[0].strip()):
1717            newx = '\n'.join(newx[1:])
1718        else:
1719            newx = '\n'.join(newx)
1720    else:
1721        newx = str(x)
1722
1723    top_line = ''
1724    if warn_level:
1725        top_line+= '''<p class="{}">{}</p>'''.format(
1726            'wcol_'+warn_level, warn_level )
1727
1728    y = ''
1729    y+= vpad*'\n'
1730    y+= '''<div>
1731    <pre {} ><left><b>{}{}</b></left></pre>
1732</div>'''.format(addclass, top_line, newx)
1733    y+= vpad*'\n'
1734
1735    return y
1736
1737# -------------------------------------------------------------------
1738
1739def read_descrip_json(x):
1740    '''Take the input json file 'x' and return an instance of the
1741apqc_item_info() class.
1742'''
1743
1744    ddd   = read_json_to_dict(x) # get json as dictionary
1745    ainfo = apqc_item_info()     # initialize obj to hold info
1746    ainfo.set_all_from_dict(ddd) # set everything in this obj that we can
1747
1748    return ainfo
1749
1750# -------------------------------------------------------------------
1751
1752def read_title_json(x):
1753    '''Take the input json file 'x' and return an instance of the
1754apqc_title_info() class.
1755'''
1756
1757    ddd   = read_json_to_dict(x) # get json as dictionary
1758    tinfo = apqc_title_info()    # initialize obj to hold title info
1759    tinfo.set_all_from_dict(ddd) # set everything in this obj that we can
1760
1761    return tinfo
1762
1763# ----------------------------------------------------------------------
1764
1765def read_dat(x, do_join=True):
1766
1767    fff = open(x, 'r')
1768    txt = fff.readlines()
1769    fff.close()
1770
1771    if do_join :
1772        out = ''.join(txt)
1773        return out
1774    else:
1775        return txt
1776
1777# ----------------------------------------------------------------------
1778
1779# check if json exists- return full or null dict
1780def read_json_to_dict(x):
1781
1782    if os.path.isfile(x):
1783        with open(x, 'r') as fff:
1784            xdict = json.load(fff)
1785    else:
1786        xdict = {}
1787
1788    return xdict
1789
1790# ----------------------------------------------------------------------
1791
1792def make_pbar_line(d, imgpbar, vpad=0, addclassdiv="", addclassimg="",
1793                   dobold=True):
1794
1795    y = '''<div {} ><pre>'''.format(addclassdiv)
1796    if dobold :
1797        y+= """<b>"""
1798
1799    # [PT: Jan 2, 2019] Typically, the pbar/cbar is for an olay, hence
1800    # the default; in some cases, we might want flexibility here,
1801    # though.
1802    voltype = "olay"
1803    if 'pbar_vol' in d :
1804        voltype = d['pbar_vol']
1805
1806    y+= """{}: {} """.format(voltype, d['pbar_bot'])
1807
1808    y+= '''<img {} '''.format(addclassimg)
1809    y+= '''style="display: inline; margin: -5 -5px;" '''
1810    y+= '''src="{}" > '''.format(imgpbar)
1811    y+= '''{} ({})'''.format(d['pbar_top'], d['pbar_comm'])
1812
1813    if 'vthr' in d :
1814        y+= '''\nthr : {}'''.format(d['vthr'])
1815        if 'vthr_reason' in d :
1816            y+= ''' ({})'''.format(d['vthr_comm'])
1817
1818    # [PT: Jan 2, 2019] can add in comments, too
1819    if 'pbar_comm' in d :
1820        if type(d['pbar_comm']) == list :
1821            for x in d['pbar_comm']:
1822                y+= '''\n{}'''.format(x)
1823        else :
1824            # assume it is unicode (esp. in py2) or str (likely in
1825            # py3)
1826            y+= '''\n{}'''.format(d['pbar_comm'])
1827
1828    if dobold :
1829        y+= '''</b>'''
1830    y+= '''</pre></div>\n'''
1831
1832    if vpad:
1833        y= """\n"""+y
1834        y+="""\n"""
1835
1836    return y
1837
1838# ----------------------------------------------------------------------
1839
1840def read_pbar_range(x, dtype="NA"):
1841
1842    fff = open(x, 'r')
1843    txt = fff.readlines()
1844    fff.close()
1845
1846    Nlines = len(txt)
1847    if not(Nlines):
1848        sys.exit("** ERROR: no lines of text in {}?".format(x))
1849    elif Nlines > 1:
1850        sys.exit("** ERROR: too many lines (={}) in {}?".format(Nlines, x))
1851
1852    l0 = txt[0]
1853    y  = l0.split()
1854
1855    Nnums = len(y)
1856    if Nnums != 3:
1857        sys.exit("** ERROR: wrong number of nums (={}) in {}?".format(Nnums, x))
1858
1859    out = []
1860    if dtype == int :
1861        for nn in y:
1862            z = int(nn)
1863            out.append(z)
1864    elif dtype == float :
1865        for nn in y:
1866            z = float(nn)
1867            out.append(z)
1868    else: # the NA or other cases...
1869        for nn in y:
1870            if nn.__contains__('.'):
1871                z = float(nn)
1872            else:
1873                z = int(nn)
1874            out.append(z)
1875
1876    return out[0], out[1], out[2]
1877
1878# ----------------------------------------------------------------------
1879