# 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: datagraf.tcl,v 2.78 2006/01/28 19:16:59 jfontain Exp $


# note: similar code is used in predictor call: make sure the implementations are synchronized


class dataGraph {

    proc dataGraph {this parentPath args} composite {[new frame $parentPath] $args} blt2DViewer {
        $widget::($this,path) [blt::stripchart $widget::($this,path).graph -title {}] 5
    } {
        set graphPath $widget::($this,path).graph
        $graphPath pen create void -linewidth 0 -symbol none                                                  ;# pen for void values
        $graphPath grid configure -minor 0
        $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 \$dataGraph::($this,-grid)"\
            -variable dataGraph::($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 "dataGraph::updateMessage $this"                               ;# delayed since it needs object to be constructed
        set ($this,graphPath) $graphPath
        set ($this,graph) $graph
        composite::complete $this
        bind $graphPath <Configure> "+ dataGraph::refresh $this"                                               ;# track size updates
    }

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

    proc iconData {} {
        return {
            R0lGODdhJAAkAKUAAPj8+Hh4eDBgIDBgGKDscJDcaIjQYHjAWDg4OHC0UGikSFiYQHh8eHBIAPDcSODQQNjEONC8MMiwMMCoKLicIAAAABBgeICEgKjU4JCY
            kLi8uIiQiJjI2IjA0NDY0KCkoIC4yKisqHCwwGikuMDEwFicsMjQyODk4Ojs6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAJAAkAAAG/kCAcCgMEI/G4zBZVBKZSijy6VxWAVKqFRsICL7dsNgrGAjGAbMZLRQQ3gKs0v0mxId0eBNf
            6BfORF9+fl+Cg4BQAgaLi3cAioyRkoxnRklhAgeamoWbnJmem18IpGKBCaipqmAIX6tnCExZjwq1trWVAbFkYF0IWnMCC8PDlXtje8dovL1jDMvQVbthwFHV
            Sg3Z2VNVULMA2Q7i4g3X1ZdiDQ/r7OsNaNrv0F1EDRD3+PnbAdn65d5HGkQYOFAbwYICDxLctkVIAwkQIcrjF7GixYrllDWYwHFCA1Igs3XsGG+kx2lJGlBY
            +e7XE5UrKUzkF1OmpYDZfEWJF4DBzxKeN5HQcxkllk8iz3btqcC0qdOnUKNKZUDVglWqWLMysHpVK1cLWhlcuGABg1kLGdKqTVvWLAYLGuJqaHtW7NgLGzZY
            4MCXA1y5c/f25ft1sF8GefOy7cCYsQUPkC00nkx5MlgGaz9otgCic2eunkF8De25q+bTIVKHsCCitevXXa3CvsxAtWrAc0fo3q2btoaqXLHilkuiuHGrJZIn
            v2y8uNbm0EmYmE7dxNavVKtr374dsnfvYb+LH0/+hPnz6NOrX88ehfv38OPLn08/CAA7
        }
    }

    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 {} {}]\
            [list -yminimum $global::graphMinimumY]\
            [list -yminimumcell {} {}]\
        ]
    }

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

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

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

    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) pen configure void -color $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 set-yminimum {this value} {
        blt2DViewer::setLimit $this minimum $value
    }
    proc set-yminimumcell {this value} {
        blt2DViewer::setLimitCell $this minimum $value
    }

    proc newElement {this path args} {                                                          ;# invoked from 2D viewer base class
        return [eval new element $path $composite::($this,-interval) $args]
    }

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

    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 dataStackedGraph 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 "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)\
                -yminimum $composite::($this,-yminimum) -yminimumcell $composite::($this,-yminimumcell)\
                -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} {
        foreach element $blt2DViewer::($this,elements) {
            element::refresh $element
        }
    }

}


class dataGraph {

    class element {

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

            if {$interval == 0} {                                               ;# history mode: whole samples set will come at once
                set ($this,start) 0
                set ($this,end) 0
            }
            blt::vector create x$this                                                                          ;# x axis data vector
            blt::vector create y$this                                                                          ;# y axis data vector
            blt::vector create weight$this                                                                    ;# weights data vector
            blt::vector create seconds$this                                                                   ;# source data to plot
            x$this notify whenidle; y$this notify whenidle; weight$this notify whenidle                   ;# try to optimize display
            # use object identifier as element identifier, and handle void values
            $path element create $this -mapx x2 -label {} -xdata x$this -ydata y$this -weight weight$this -styles {{void 0 0}}\
                -pixels 1 -symbol splus                               ;# so that lone valid sample among voids is displayed as a dot
            set ($this,path) $path
            switched::complete $this
        }

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

            blt::vector destroy x$this y$this weight$this seconds$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 -color $value
        }

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

        proc append {this seconds value} {
            variable seconds$this
            variable y$this
            variable weight$this

            if {$value eq "?"} {                                                                                   ;# void new value
                if {[seconds$this length] == 0} return                                                ;# wait till first valid value
                seconds$this append [seconds$this index end]                                              ;# double last known value
                y$this append [y$this index end]
                weight$this append 0                                                                  ;# and display with void style
            } else {                                                                                                        ;# valid
                seconds$this append $seconds
                y$this append $value
                weight$this append 1                                                                                 ;# normal style
            }
        }

        proc refresh {this } {
            variable x$this
            variable seconds$this

            if {[seconds$this length] == 0} return
            ::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
            x$this expr "round((seconds$this - $maximum) * $pixelsPerSecond)"
        }

        proc update {this seconds value} {                      ;# real time mode: value is either a valid number or the ? character
            variable seconds$this
            variable y$this
            variable weight$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; y$this delete :$length; weight$this delete :$length}
            refresh $this
        }

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

            seconds$this set {}; y$this set {}; weight$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 y$this
            variable weight$this

            seconds$this set {}; y$this set {}; weight$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
        }

    }

}
