# copyright (C) 1997-2006 Jean-Luc Fontaine (mailto:jfontain@free.fr)
# this program is free software: please read the COPYRIGHT file enclosed in this package or use the Help Copyright menu

# $Id: stagraph.tcl,v 2.70 2006/01/28 19:16:59 jfontain Exp $


class dataStackedGraph {

    proc dataStackedGraph {this parentPath args} composite {[new frame $parentPath] $args} blt2DViewer {
        $widget::($this,path) [blt::barchart $widget::($this,path).graph -title {} -barmode stack -barwidth 0] 0 1
    } {
        set graphPath $widget::($this,path).graph
        $graphPath x2axis configure -max 0 -showticks 0 -hide true
        set graph [new bltGraph $graphPath $this]
        bltGraph::hideAxisAndCrossHair $graph 1
        $blt2DViewer::($this,menu) add checkbutton\
            -label $bltGraph::(menu,grid,label) -command "composite::configure $this -grid \$dataStackedGraph::($this,-grid)"\
            -variable dataStackedGraph::($this,-grid) -offvalue 0 -onvalue 1                         ;# add grid toggling menu entry
        menuContextHelp::set $blt2DViewer::($this,help) [$blt2DViewer::($this,menu) index end] $bltGraph::(menu,grid,help)
        after idle "dataStackedGraph::updateMessage $this"                        ;# delayed since it needs object to be constructed
        set ($this,graphPath) $graphPath
        set ($this,graph) $graph
        composite::complete $this
        bind $graphPath <Configure> "+ dataStackedGraph::refresh $this 0"                                      ;# track size updates
    }

    proc ~dataStackedGraph {this} {
        delete $($this,graph)
        if {$composite::($this,-deletecommand) ne ""} {
            uplevel #0 $composite::($this,-deletecommand)                                   ;# always invoke command at global level
        }
    }

    proc iconData {} {
        return {
            R0lGODdhJAAkAKUAAPj8+Hh4eDBgIDBgGHh8eKDocKjweJjgaJDYaIjQYAAAAIDIWHjAWHi4UHCwUHBIAGioSPDYSGCgQOjQSFiYQODISFCQOODESNi8SNC4
            SNCwSMisSMCkSMCcSLiYSBBgeLCQSLDY6ICEgKDQ4LCMSIiQiJjI2KiESJCYkIjA0KCkoIC4yKisqHiwwLi8uGiouMDEwGCgsMjQyNDY0ODk4Ojs6AAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAJAAkAAAG/kCAcCgMEI/G4zBZVBKZSijy6RQ0lQLBwHrcbpdFglhQEBAC4jRBYGgbzGm22xyoQ8nlO77A
            L2f3fGYAUgACB4dchYeLjI2LgoQCCJMIf5SXmJmQRWhrCZ+goaKjoQIKYnVEAgusra6vsK4CRpEMtre4ubq4s4NYDcDBwsPEwpuDnQIOy8zNzs/NpqhQCg8Q
            EA/Z2dfc3d7cvYQPEdkR5uYPEurr7O0S4UoPE/P09A8U+Pna9/kUx3ViHlQYSLBgNgsWshl8gNCCtDNQHlyYSLHixGwWKWqDd+QBho8gQ4ocGfIBrXgZUqpc
            ybLlygcEfCELqKGmzZs4c958cAoi8JEHG4IKHUq06FCTMn9yWMq0qdOnTZGK60C1qtWrWK3ClKmgazUPYMOK9fCh7IexZM3y9NozYFkQcOPC/RCibogPcuna
            /aCGgIi/Ij6MGPGBhGHDZQcrJmx2MWG/gEtILvHBhOUPJzJXtsy5s+fLBCaXQEEaxYcUqFOYTc26tWu+pVGomK3iw4rbuHPr3p2bL20VLIKz+NCiuPHjyJMf
            5yuchYvnLj68mE69uvXr1flCdwGjO4wPMcKLH0++/Hi+3mHIWC/DrPv38OOXJcBexoz79/vq38+fAP4ZNAQo4IAEFmjggTUkqOCCDDbo4INBAAA7
        }
    }

    proc options {this} {
        set samples [expr {$global::graphNumberOfIntervals + 1}]
        return [list\
            [list -cellcolors {} {}]\
            [list -deletecommand {} {}]\
            [list -draggable 0 0]\
            [list -grid $global::graphDisplayGrid]\
            [list -height $global::viewerHeight]\
            [list -interval 0 0]\
            [list -labelsposition right]\
            [list -plotbackground $global::graphPlotBackground]\
            [list -samples $samples $samples]\
            [list -width $global::viewerWidth]\
            [list -xlabelsrotation $global::graphXAxisLabelsRotation]\
            [list -ymaximum {} {}]\
            [list -ymaximumcell {} {}]\
        ]
    }

    proc set-cellcolors {this value} {                             ;# colors of soon to be created cells when initializing from file
        if {$composite::($this,complete)} {
            error {option -cellcolors cannot be set dynamically}
        }
        blt2DViewer::setCellColors $this $value
    }

    proc set-deletecommand {this value} {}

    proc set-grid {this value} {
        if {$value} {
            $($this,graphPath) grid configure -hide no
        } else {
            $($this,graphPath) grid configure -hide yes
        }
        set ($this,-grid) $value                                                ;# so that corresponding popup menu entry is updated
    }

    foreach option {-height -width} {
        proc set$option {this value} "\$widget::(\$this,path) configure $option \$value"
    }

    proc set-draggable {this value} {
        if {$composite::($this,complete)} {
            error {option -draggable cannot be set dynamically}
        }
        if {$value} {
            blt2DViewer::allowDrag $this
            bltGraph::allowDrag $($this,graph) $blt2DViewer::($this,drag)                                     ;# extend drag formats
        }
    }

    proc set-interval {this value} {
        set graph $($this,graph)
        bltGraph::setRange $graph [expr {($composite::($this,-samples) - 1) * $value}]
        bltGraph::xAxisUpdateRange $graph
        bltGraph::xUpdateGraduations $graph
        refresh $this 1
    }

    proc set-samples {this value} {                                                                     ;# stored at composite level
        if {$composite::($this,-interval) == 0} return                                           ;# useless in database history mode
        set graph $($this,graph)
        bltGraph::setRange $graph [expr {($value - 1) * $composite::($this,-interval)}]
        bltGraph::xAxisUpdateRange $graph
        bltGraph::xUpdateGraduations $graph
    }

    proc set-labelsposition {this value} {
        blt2DViewer::updateLayout $this
    }

    proc set-plotbackground {this value} {
        $($this,graphPath) configure -plotbackground $value
        $($this,graphPath) grid configure -color [visibleForeground $value]
    }

    proc set-xlabelsrotation {this value} {
        bltGraph::xRotateLabels $($this,graph) $value
    }

    proc set-ymaximum {this value} {
        blt2DViewer::setLimit $this maximum $value
    }
    proc set-ymaximumcell {this value} {
        blt2DViewer::setLimitCell $this maximum $value
    }

    proc updateTimeDisplay {this seconds} {
        bltGraph::xAxisUpdateRange $($this,graph) $seconds
    }

    proc newElement {this path args} {                                                          ;# invoked from 2D viewer base class
        return [eval new element $path $args]
    }

    proc updateElement {this element seconds value} {                           ;# value is either a valid number or the ? character
        element::update $element $seconds $value
    }

    proc canMonitor {this array} {
        if {$composite::($this,-interval) > 0} {
            return 1                                                                                             ;# not history mode
        } else {                              ;# check that cells belong to instance module, and not summary table data, for example
            return [string equal [lindex [modules::decoded [modules::namespaceFromArray $array]] 0] instance]
        }
    }

    proc update {this array} {                      ;# update display using cells data (implementation identical to dataGraph class)
        if {$composite::($this,-interval) > 0} {                                                                 ;# not history mode
            return [blt2DViewer::_update $this $array]                                                    ;# use base implementation
        }
        foreach {minimum maximum} [databaseInstances::cursorsRange] {}                                                 ;# in seconds
        set graph $($this,graph)
        bltGraph::setRange $graph [expr {$maximum - $minimum}]
        bltGraph::xUpdateGraduations $graph
        bltGraph::xAxisUpdateRange $graph $maximum
        foreach element $blt2DViewer::($this,elements) {                                                             ;# history mode
            set cell $blt2DViewer::($this,cell,$element)
            foreach {start end} [databaseInstances::range $cell] {}           ;# database cell range in seconds (limited by cursors)
            if {$start eq ""} {                                                                                   ;# no history data
                element::range $element {}                                                                       ;# let element know
                continue
            }
            set start [clock scan $start]
            set end [clock scan $end]
            if {($element::($element,start) == $start) && ($element::($element,end) == $end)} {
                element::refresh $element                               ;# no range change: avoid potentially lengthy database query
            } else {
                element::range $element [databaseInstances::history $cell]                                ;# (inside cursors limits)
            }
        }
        if {[info exists cell]} {                                                 ;# no maximum can exist when there are no elements
            blt2DViewer::updateLimit $this maximum $array                      ;# note: maximum cell will take value at end of range
            blt2DViewer::yAxisUpdate $this                    ;# since vertical axis position can change due to tick labels changing
        }
    }

    proc updateRealTimeHistory {this element array row column} {
        set list [history::list $array $row $column]
        set last [lindex $list end-1]
        if {$last ne ""} {
            updateTimeDisplay $this $last
        }                                                                                               ;# else there was no history
        element::list $element $list
    }

    proc modified {this monitored} {                                                                    ;# number of monitored cells
        bltGraph::hideAxisAndCrossHair $($this,graph) [expr {$monitored == 0}]                     ;# hide if no elements to display
        updateMessage $this
    }

    proc updateMessage {this} {
        if {[llength [blt2DViewer::cells $this]] == 0} {
            centerMessage $widget::($this,path)\
                [mc "stacked graph chart:\ndrop data cell(s)"] $composite::($this,-plotbackground) $global::viewerMessageColor
        } else {
            centerMessage $widget::($this,path) {}
        }
    }

    proc initializationConfiguration {this} {
        return [concat\
            [list\
                -ymaximum $composite::($this,-ymaximum) -ymaximumcell $composite::($this,-ymaximumcell)\
                -labelsposition $composite::($this,-labelsposition) -grid $composite::($this,-grid)\
            ] [blt2DViewer::_initializationConfiguration $this]\
        ]
    }

    virtual proc updateLabels {this} {
        blt2DViewer::_updateLabels $this [expr {$composite::($this,-interval) > 0}]             ;# no values display in history mode
    }

    proc refresh {this force} {                  ;# assumes that size is correct (update idletasks may have been previously invoked)
        set width [$($this,graphPath) extents plotwidth]
        if {$force} {
            set refresh 1
        } elseif {[info exists ($this,plotwidth)]} {
            set refresh [expr {$width != $($this,plotwidth)}]                ;# refresh only when necessary as this is rather costly
        } else {
            set refresh 1
        }
        set ($this,plotwidth) $width
        if {!$refresh} return
        foreach element $blt2DViewer::($this,elements) {
            element::refresh $element
        }
    }

}


class dataStackedGraph {

    class element {

        proc element {this path args} switched {$args} {
            variable x$this
            variable y$this
            variable seconds$this
            variable values$this

            blt::vector create x$this                                                                          ;# x axis data vector
            blt::vector create y$this                                                                          ;# y axis data vector
            blt::vector create seconds$this                                                                   ;# source data to plot
            blt::vector create values$this
            x$this notify whenidle; y$this notify whenidle
            # use object identifier as element identifier and make sure bar width is 1 pixel:
            $path element create $this -mapx x2 -label {} -xdata x$this -ydata y$this -borderwidth 0 -barwidth 0.001
            set ($this,start) 0; set ($this,end) 0                                                      ;# used in history mode only
            set ($this,path) $path
            switched::complete $this
        }

        proc ~element {this} {
            variable x$this
            variable y$this
            variable seconds$this
            variable values$this

            blt::vector destroy x$this y$this seconds$this values$this
            $($this,path) element delete $this
            if {$switched::($this,-deletecommand) ne ""} {
                uplevel #0 $switched::($this,-deletecommand)                                ;# always invoke command at global level
            }
        }

        proc options {this} {
            return [::list\
                [::list -color black black]\
                [::list -deletecommand {} {}]\
            ]
        }

        proc set-color {this value} {
            $($this,path) element configure $this -foreground $value
        }

        proc set-deletecommand {this value} {}                                                   ;# data is stored at switched level

        proc append {this seconds value} {                                                                  ;# append to source data
            variable seconds$this
            variable values$this

            if {$value eq "?"} {
               set value Inf                                               ;# void new value: flag using with impossible valid value
            } else {
                set value [expr {abs($value)}]                                                      ;# negative values not supported
            }
            if {([seconds$this length] > 0) && ($seconds == int([seconds$this index end]))} {
                values$this index end $value                                                             ;# update of existing point
            } else {
                seconds$this append $seconds
                values$this append $value
            }
        }

        proc refresh {this } {                                                                                     ;# displayed data
            variable x$this
            variable y$this
            variable seconds$this
            variable values$this

            set length [seconds$this length]
            if {$length == 0} return                                                                                      ;# no data
            ::update idletasks                              ;# this actually results in less redundant invocations of this procedure
            set path $($this,path)
            set minimum [$path xaxis cget -min]; set maximum [$path xaxis cget -max]
            if {($minimum eq "") || ($maximum eq "")} return                                             ;# happens in database mode
            set width [$path extents plotwidth]
            if {$width <= 2} return                                                  ;# nothing can be plotted in such a small space
            set pixelsPerSecond [expr {double($width) / ($maximum - $minimum)}]
            $path x2axis configure -min -$width
            blt::vector create pixels                                                                ;# scaled abscissas from source
            pixels expr "(seconds$this - $maximum) * $pixelsPerSecond"
            blt::vector create add
            blt::vector create limits
            x$this set {}; y$this set {}                                                                         ;# reset then fill:
            for {set index 0} {$index < $length} {incr index} {
                set x [expr {round([pixels index $index])}]                                                   ;# pixels are integers
                set y [values$this index $index]                           ;# note: void values (stored as infinite) are not plotted
                if {$y != Inf} {                                                                           ;# current value is valid
                    if {[info exists xLast]} {
                        if {$x == $xLast} {                          ;# case where several samples end up at the same abscissa pixel
                            if {([x$this length] == 0) || ($x != [x$this index end])} {                           ;# not yet plotted
                                x$this append $x; y$this append $y
                            } elseif {$y > [y$this index end]} {                ;# already plotted: only display largest valid value
                                y$this index end $y
                            }
                        } elseif {($yLast == Inf) || ($x == ($xLast + 1))} {  ;# no filling if at exactly next pixel or edge of hole
                            x$this append $x; y$this append $y
                        } else {                                                                                 ;# filling required
                            add seq $xLast $x                                            ;# note: sequence contains both extremities
                            add delete 0                                                                     ;# remove last abscissa
                            x$this append add
                            limits set [::list $yLast $y]
                            limits populate add [expr {[add length] - 1}]                             ;# note: resets the add vector
                            add delete 0
                            y$this append add
                        }
                    } else {
                        x$this append $x; y$this append $y
                    }
                }
                set xLast $x; set yLast $y
            }
            blt::vector destroy pixels add limits
        }

        proc update {this seconds value} {                      ;# real time mode: value is either a valid number or the ? character
            variable seconds$this
            variable values$this

            append $this $seconds $value
            set length [llength [seconds$this search 0 [$($this,path) xaxis cget -min]]]
            incr length -2     ;# save memory by removing points to the left of the x axis minimum (fails when there is little data)
            catch {seconds$this delete :$length; values$this delete :$length}
            refresh $this
        }

        proc list {this values} {                                        ;# list of instant (in seconds), value, instant, value, ...
            variable seconds$this
            variable values$this

            seconds$this set {}; values$this set {}
            foreach {seconds value} $values {                                      ;# assumes that instants are ordered increasingly
                append $this $seconds $value
            }
            refresh $this
        }

        proc range {this list} {            ;# database mode: set a whole set of points at once. list is flat: timestamp, value, ...
            variable seconds$this
            variable values$this

            seconds$this set {}; values$this set {}
            foreach {stamp value} $list {                                        ;# assumes that timestamps are ordered increasingly
                if {$value eq ""} {                       ;# void value (instead of ? since data is assumed to come from a database)
                    append $this [clock scan $stamp] ?
                } else {
                    append $this [clock scan $stamp] $value
                }
            }
            if {[llength $list] == 0} {                                                                         ;# no samples at all
                set ($this,start) 0
                set ($this,end) 0
            } else {                                                       ;# remember whole data (even including voids) time limits
                set ($this,start) [clock scan [lindex $list 0]]                                 ;# remember range extents separately
                set ($this,end) [clock scan [lindex $list end-1]]              ;# (since null values are not saved in vectors above)
            }
            refresh $this
        }

    }

}
