source: branches/1.3/gui/scripts/resultset.tcl @ 5256

Last change on this file since 5256 was 3827, checked in by ldelgass, 11 years ago

Merge mmc's fixes from r3770 in the trunk

File size: 20.9 KB
Line 
1# -*- mode: tcl; indent-tabs-mode: nil -*-
2# ----------------------------------------------------------------------
3#  COMPONENT: ResultSet - set of XML objects for simulated results
4#
5#  This data structure collects all of the simulated results
6#  produced by a series of tool runs.  It is used by the Analyzer,
7#  ResultSelector, and other widgets to keep track of all known runs
8#  and visualize the result that is currently selected.  Each run
9#  has an index number ("#1", "#2", "#3", etc.) that can be used to
10#  label the run and refer to it later.
11# ======================================================================
12#  AUTHOR:  Michael McLennan, Purdue University
13#  Copyright (c) 2004-2012  HUBzero Foundation, LLC
14#
15#  See the file "license.terms" for information on usage and
16#  redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
17# ======================================================================
18package require Itcl
19
20itcl::class Rappture::ResultSet {
21    constructor {args} { # defined below }
22    destructor { # defined below }
23
24    public method add {xmlobj}
25    public method clear {{xmlobj ""}}
26    public method diff {option args}
27    public method find {collist vallist}
28    public method get {collist xmlobj}
29    public method contains {xmlobj}
30    public method size {}
31
32    public method notify {option args}
33    protected method _notifyHandler {args}
34
35    protected method _addOneResult {tuples xmlobj {simnum ""}}
36
37    private variable _dispatcher ""  ;# dispatchers for !events
38    private variable _results ""     ;# tuple of known results
39    private variable _resultnum 0    ;# counter for result #1, #2, etc.
40    private variable _notify         ;# info used for notify command
41}
42                                                                               
43# ----------------------------------------------------------------------
44# CONSTRUCTOR
45# ----------------------------------------------------------------------
46itcl::body Rappture::ResultSet::constructor {args} {
47    # create a dispatcher for events
48    Rappture::dispatcher _dispatcher
49    $_dispatcher register !change
50    $_dispatcher dispatch $this !change \
51        [itcl::code $this _notifyHandler]
52
53    # create a list of tuples for data
54    set _results [Rappture::Tuples ::#auto]
55    $_results column insert end -name xmlobj -label "top-level XML object"
56    $_results column insert end -name simnum -label "simulation number"
57
58    # clear notification info
59    set _notify(ALL) ""
60
61    eval configure $args
62}
63
64# ----------------------------------------------------------------------
65# DESTRUCTOR
66# ----------------------------------------------------------------------
67itcl::body Rappture::ResultSet::destructor {} {
68    clear
69    itcl::delete object $_results
70}
71
72# ----------------------------------------------------------------------
73# USAGE: add <xmlobj>
74#
75# Adds a new result to this result set.  Scans through all existing
76# results to look for a difference compared to previous results.
77# Returns the simulation number (#1, #2, #3, etc.) of this new result
78# to the caller.  The various data objects for this result set should
79# be added to their result viewers at the same index.
80# ----------------------------------------------------------------------
81itcl::body Rappture::ResultSet::add {xmlobj} {
82    set xmlobj0 [$_results get -format xmlobj end]
83    if {$xmlobj0 eq ""} {
84        #
85        # If this is the first result, then there are no diffs.
86        # Add it right in.
87        #
88        set simnum "#[incr _resultnum]"
89        $_results insert end [list $xmlobj $simnum]
90    } else {
91        #
92        # For all later results, find the diffs and add any new columns
93        # into the results tuple.  The latest result is the most recent.
94        #
95        set simnum [_addOneResult $_results $xmlobj]
96    }
97
98    # make sure we fix up associated controls
99    $_dispatcher event -now !change op add what $xmlobj
100
101    return $simnum
102}
103
104# ----------------------------------------------------------------------
105# USAGE: clear ?<xmlobj>?
106#
107# Clears one or all results in this result set.  If no specific
108# result object is specified, then all results are cleared.
109# ----------------------------------------------------------------------
110itcl::body Rappture::ResultSet::clear {{xmlobj ""}} {
111    if {$xmlobj ne ""} {
112        #
113        # Delete just one result.  Look for the result among the
114        # tuples and remove it.  Then, rebuild all of the tuples
115        # by scanning back through them and building them back up.
116        # This will rebuild the columns/controls as they should
117        # be now, removing anything that is no longer necessary.
118        #
119        set irun [$_results find -format xmlobj $xmlobj]
120        if {[llength $irun] == 1} {
121            # grab a description of what we're about to delete
122            set dlist [list simnum [$_results get -format simnum $irun]]
123            foreach col [lrange [$_results column names] 2 end] {
124                set raw [lindex [Rappture::LibraryObj::value $xmlobj $col] 0]
125                lappend dlist $col $raw  ;# use "raw" (user-readable) label
126            }
127
128            # delete this from the tuples of all results
129            itcl::delete object $xmlobj
130            $_results delete $irun
131
132            set new [Rappture::Tuples ::#auto]
133            $new column insert end -name xmlobj -label "top-level XML object"
134            $new column insert end -name simnum -label "simulation number"
135
136            for {set n 0} {$n < [$_results size]} {incr n} {
137                set rec [lindex [$_results get -format {xmlobj simnum} $n] 0]
138                foreach {obj num} $rec break
139                if {$n == 0} {
140                    $new insert end [list $obj $num]
141                } else {
142                    _addOneResult $new $obj $num
143                }
144            }
145
146            # plug in the new set of rebuilt tuples
147            itcl::delete object $_results
148            set _results $new
149
150            # make sure we fix up associated controls at some point
151            $_dispatcher event -now !change op clear what $dlist
152        }
153    } else {
154        #
155        # Delete all results.
156        #
157        for {set irun 0} {$irun < [$_results size]} {incr irun} {
158            set xo [$_results get -format xmlobj $irun]
159            itcl::delete object $xo
160        }
161        $_results delete 0 end
162
163        # make sure we fix up associated controls at some point
164        $_dispatcher event -now !change op clear what all
165    }
166
167    if {[$_results size] == 0} {
168        # no results left?  then reset to a clean state
169        eval $_results column delete [lrange [$_results column names] 2 end]
170        set _resultnum 0
171    }
172}
173
174# ----------------------------------------------------------------------
175# USAGE: diff names
176# USAGE: diff values <column> ?<which>?
177#
178# Returns information about the diffs in the current set of known
179# results.  The "diff names" returns a list of column names for
180# parameters that have diffs.  (These are the columns in the tuples.)
181#
182# The "diff values" returns the various values associated with a
183# particular <column> name.  If the optional <which> is specified,
184# then it is treated as an index into the list of values--0 for the
185# first value, 1 for the second, etc.  Each value is returned as
186# a list with two words.  The first is the the label associated with
187# the value.  The second is the normalized (numeric) value, which can
188# be sorted to get a particular ordering.
189# ----------------------------------------------------------------------
190itcl::body Rappture::ResultSet::diff {option args} {
191    switch -- $option {
192        names {
193            return [$_results column names]
194        }
195        values {
196            if {[llength $args] < 1} {
197                error "wrong # args: should be \"diff values col ?which?\""
198            }
199            set col [lindex $args 0]
200
201            set which "all"
202            if {[llength $args] > 1} {
203                set which [lindex $args 1]
204            }
205
206            set rlist ""
207            # build an array of normalized values and their labels
208            if {$col == "simnum"} {
209                set nruns [$_results size]
210                for {set n 0} {$n < $nruns} {incr n} {
211                    set simnum [$_results get -format simnum $n]
212                    lappend rlist $simnum $n
213                }
214            } else {
215                set havenums 1
216                foreach rec [$_results get -format [list xmlobj $col]] {
217                    set xo [lindex $rec 0]
218                    set v [lindex $rec 1]
219                    foreach {raw norm} \
220                        [Rappture::LibraryObj::value $xo $col] break
221
222                    if {![info exists unique($v)]} {
223                        # keep only unique label strings
224                        set unique($v) $norm
225                    }
226                    if {$havenums && ![string is double $norm]} {
227                        set havenums 0
228                    }
229                }
230
231                if {!$havenums} {
232                    # don't have normalized nums? then sort and create nums
233                    set rlist ""
234                    set n 0
235                    foreach val [lsort -dictionary [array names unique]] {
236                        lappend rlist $val [incr n]
237                    }
238                } else {
239                    set rlist [array get unique]
240                }
241            }
242
243            if {$which eq "all"} {
244                return $rlist
245            }
246
247            # treat the "which" parameter as an XML object
248            set irun [lindex [$_results find -format xmlobj $which] 0]
249            if {$irun ne ""} {
250                set val [lindex [$_results get -format $col $irun] 0]
251                array set val2norm $rlist
252                if {[info exists val2norm($val)]} {
253                    return [list $val $val2norm($val)]
254                }
255            }
256        }
257        default {
258            error "bad option \"$option\": should be names or values"
259        }
260    }
261}
262
263# ----------------------------------------------------------------------
264# USAGE: find <columnList> <valueList>
265#
266# Searches through the results for a set of tuple values that match
267# the <valueList> for the given <columnList>.  Returns a list of
268# matching xml objects or "" if there is no match.  If the <valueList>
269# is *, then it returns a list of all xml objects.
270# ----------------------------------------------------------------------
271itcl::body Rappture::ResultSet::find {collist vallist} {
272    if {$vallist eq "*"} {
273        return [$_results get -format xmlobj]
274    }
275
276    set rlist ""
277    foreach irun [$_results find -format $collist -- $vallist] {
278        lappend rlist [$_results get -format xmlobj $irun]
279    }
280    return $rlist
281}
282
283# ----------------------------------------------------------------------
284# USAGE: get <columnList> <xmlobj>
285#
286# Returns values for the specified <columnList> for the given <xmlobj>.
287# This is a way of querying associated data for the given object.
288# ----------------------------------------------------------------------
289itcl::body Rappture::ResultSet::get {collist xmlobj} {
290    set irun [lindex [$_results find -format xmlobj $xmlobj] 0]
291    if {$irun ne ""} {
292        return [lindex [$_results get -format $collist $irun] 0]
293    }
294    return ""
295}
296
297# ----------------------------------------------------------------------
298# USAGE: contains <xmlobj>
299#
300# Checks to see if the given <xmlobj> is already represented by
301# some result in this result set.  This comes in handy when checking
302# to see if an input case is already covered.
303#
304# Returns 1 if the result set already contains this result, and
305# 0 otherwise.
306# ----------------------------------------------------------------------
307itcl::body Rappture::ResultSet::contains {xmlobj} {
308    # no results? then this must be new
309    if {[$_results size] == 0} {
310        return 0
311    }
312
313    #
314    # Compare this new object against the last XML object in the
315    # results set.  If it has a difference, make sure that there
316    # is a column to represent the quantity with the difference.
317    #
318    set xmlobj0 [$_results get -format xmlobj end]
319    foreach {op vpath oldval newval} [$xmlobj0 diff $xmlobj] {
320        if {[$xmlobj get $vpath.about.diffs] == "ignore"} {
321            continue
322        }
323        if {$op == "+" || $op == "-"} {
324            # ignore differences where parameters come and go
325            # such differences make it hard to work controls
326            continue
327        }
328        if {[$_results column names $vpath] == ""} {
329            # no column for this quantity yet
330            return 0
331        }
332    }
333
334    #
335    # If we got this far, then look through existing results for
336    # matching tuples, then check each one for diffs.
337    #
338    set format ""
339    set tuple ""
340    foreach col [lrange [$_results column names] 2 end] {
341        lappend format $col
342        set raw [lindex [Rappture::LibraryObj::value $xmlobj $col] 0]
343        lappend tuple $raw  ;# use the "raw" (user-readable) label
344    }
345    if {[llength $format] > 0} {
346        set ilist [$_results find -format $format -- $tuple]
347    } else {
348        set ilist 0  ;# no diffs -- must match first entry
349    }
350
351    foreach i $ilist {
352        set xmlobj0 [$_results get -format xmlobj $i]
353        set diffs [$xmlobj0 diff $xmlobj]
354        if {[llength $diffs] == 0} {
355            # no diffs -- already contained here
356            return 1
357        }
358    }
359
360    # must be some differences
361    return 0
362}
363
364
365# ----------------------------------------------------------------------
366# USAGE: size
367#
368# Returns the number of results currently stored in the set.
369# ----------------------------------------------------------------------
370itcl::body Rappture::ResultSet::size {} {
371    return [$_results size]
372}
373
374# ----------------------------------------------------------------------
375# USAGE: notify add <client> ?!event !event ...? <command>
376# USAGE: notify get ?<client>? ?!event?
377# USAGE: notify remove <client> ?!event !event ...?
378#
379# Clients use this to add/remove requests for notifications about
380# various events that signal changes to the data in each ResultSet.
381#
382# The "notify add" operation takes a <client> name (any unique string
383# identifying the client), an optional list of events, and the <command>
384# that should be called for the callback.
385#
386# The "notify get" command returns information about clients and their
387# registered callbacks.  With no args, it returns a list of <client>
388# names.  If the <client> is specified, it returns a list of !events.
389# If the <client> and !event is specified, it returns the <command>.
390#
391# The "notify remove" command removes any callback associated with
392# a given <client>.  If no particular !events are specified, then it
393# removes callbacks for all events.
394# ----------------------------------------------------------------------
395itcl::body Rappture::ResultSet::notify {option args} {
396    set allEvents {!change}
397    switch -- $option {
398        add {
399            if {[llength $args] < 2} {
400                error "wrong # args: should be \"notify add caller ?!event !event ...? command"
401            }
402            set caller [lindex $args 0]
403            set command [lindex $args end]
404            if {[llength $args] > 2} {
405                set events [lrange $args 1 end-1]
406            } else {
407                set events $allEvents
408            }
409
410            foreach name $events {
411                if {[lsearch -exact $allEvents $name] < 0} {
412                    error "bad event \"$name\": should be [join $allEvents ,]"
413                }
414                if {[lsearch $_notify(ALL) $caller] < 0} {
415                    lappend _notify(ALL) $caller
416                }
417                set _notify($caller-$name) $command
418            }
419        }
420        get {
421            switch -- [llength $args] {
422                0 {
423                    return $_notify(ALL)
424                }
425                1 {
426                    set caller [lindex $args 0]
427                    set rlist ""
428                    foreach key [array names _notify $caller-*] {
429                        lappend rlist [lindex [split $key -] end]
430                    }
431                    return $rlist
432                }
433                2 {
434                    set caller [lindex $args 0]
435                    set name [lindex $args 1]
436                    if {[info exists _notify($caller-$name)]} {
437                        return $_notify($caller-$name)
438                    }
439                    return ""
440                }
441                default {
442                    error "wrong # args: should be \"notify get ?caller? ?!event?\""
443                }
444            }
445        }
446        remove {
447            if {[llength $args] < 1} {
448                error "wrong # args: should be \"notify remove caller ?!event !event ...?"
449            }
450            set caller [lindex $args 0]
451            if {[llength $args] > 1} {
452                set events [lrange $args 1 end]
453            } else {
454                set events $allEvents
455            }
456
457            foreach name $events {
458                catch {unset _notify($caller-$name)}
459            }
460            if {[llength [array names _notify $caller-*]] == 0} {
461                set i [lsearch $_notify(ALL) $caller]
462                if {$i >= 0} {
463                    set _notify(ALL) [lreplace $_notify(ALL) $i $i]
464                }
465            }
466        }
467        default {
468            error "wrong # args: should be add, get, remove"
469        }
470    }
471}
472
473# ----------------------------------------------------------------------
474# USAGE: _notifyHandler ?<eventArgs>...?
475#
476# Called automatically whenever a !change event is triggered in this
477# object.  Scans through the list of clients that want to receive this
478# event and executes each of their callbacks.
479# ----------------------------------------------------------------------
480itcl::body Rappture::ResultSet::_notifyHandler {args} {
481    array set data $args
482    set event $data(event)
483
484    foreach caller $_notify(ALL) {
485        if {[info exists _notify($caller-$event)]} {
486            if {[catch {uplevel #0 $_notify($caller-$event) $args} result]} {
487                # anything go wrong? then throw a background error
488                bgerror "$result\n(while dispatching $event to $caller)"
489            }
490        }
491    }
492}
493
494# ----------------------------------------------------------------------
495# USAGE: _addOneResult <tuples> <xmlobj> ?<simNum>?
496#
497# Used internally to add one new <xmlobj> to the given <tuples>
498# object.  If the new xmlobj contains different input parameters
499# that are not already columns in the tuple, then this routine
500# creates the new columns.  If the optional <simNum> is specified,
501# then it is added as the simulation number #1, #2, #3, etc.  If
502# not, then the new object is automatically numbered.
503# ----------------------------------------------------------------------
504itcl::body Rappture::ResultSet::_addOneResult {tuples xmlobj {simnum ""}} {
505    #
506    # Compare this new object against the last XML object in the
507    # results set.  If it has a difference, make sure that there
508    # is a column to represent the quantity with the difference.
509    #
510    set xmlobj0 [$tuples get -format xmlobj end]
511    foreach {op vpath oldval newval} [$xmlobj0 diff $xmlobj] {
512        if {[$xmlobj get $vpath.about.diffs] == "ignore"} {
513            continue
514        }
515        if {$op == "+" || $op == "-"} {
516            # ignore differences where parameters come and go
517            # such differences make it hard to work controls
518            continue
519        }
520
521        # make sure that these values really are different
522        set oldval [lindex [Rappture::LibraryObj::value $xmlobj0 $vpath] 0]
523        set newval [lindex [Rappture::LibraryObj::value $xmlobj $vpath] 0]
524
525        if {$oldval != $newval && [$tuples column names $vpath] == ""} {
526            # no column for this quantity yet
527            $tuples column insert end -name $vpath -default $oldval
528        }
529    }
530
531    # build a tuple for this new object
532    set cols ""
533    set tuple ""
534    foreach col [lrange [$tuples column names] 2 end] {
535        lappend cols $col
536        set raw [lindex [Rappture::LibraryObj::value $xmlobj $col] 0]
537        lappend tuple $raw  ;# use the "raw" (user-readable) label
538    }
539
540    # find a matching tuple? then replace it -- only need one
541    if {[llength $cols] > 0} {
542        set ilist [$tuples find -format $cols -- $tuple]
543    } else {
544        set ilist 0  ;# no diffs -- must match first entry
545    }
546
547    # add all remaining columns for this new entry
548    set tuple [linsert $tuple 0 $xmlobj]
549    set cols [linsert $cols 0 "xmlobj"]
550
551    if {[llength $ilist] > 0} {
552        if {[llength $ilist] > 1} {
553            error "why so many matching results?"
554        }
555
556        # overwrite the first matching entry
557        # start by freeing the old result
558        set index [lindex $ilist 0]
559        set xo [$tuples get -format xmlobj $index]
560        itcl::delete object $xo
561
562        # put this new result in its place
563        $tuples put -format $cols $index $tuple
564        set simnum [$tuples get -format simnum $index]
565    } else {
566        if {$simnum eq ""} {
567            set simnum "#[incr _resultnum]"
568        }
569        set tuple [linsert $tuple 1 $simnum]
570        $tuples insert end $tuple
571    }
572    return $simnum
573}
Note: See TracBrowser for help on using the repository browser.