source: trunk/gui/scripts/imageresult.tcl @ 1555

Last change on this file since 1555 was 1552, checked in by mmc, 15 years ago

Fixed the image result viewer so that images are centered in the view.
This answers wish http://nanohub.org/wishlist/general/1/wish/44, which
was put in by the summer student working on DRInet.

File size: 18.5 KB
Line 
1# ----------------------------------------------------------------------
2#  COMPONENT: imageresult - picture image in a ResultSet
3#
4#  This widget displays an image found in the output of a Rappture
5#  tool run.  Use the "add" and "delete" methods to control the images
6#  showing in the widget.
7# ======================================================================
8#  AUTHOR:  Michael McLennan, Purdue University
9#  Copyright (c) 2004-2005  Purdue Research Foundation
10#
11#  See the file "license.terms" for information on usage and
12#  redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
13# ======================================================================
14package require Itk
15package require BLT
16package require Img
17
18option add *ImageResult.width 3i widgetDefault
19option add *ImageResult.height 3i widgetDefault
20option add *ImageResult.controlBackground gray widgetDefault
21option add *ImageResult.font \
22    -*-helvetica-medium-r-normal-*-12-* widgetDefault
23
24itcl::class Rappture::ImageResult {
25    inherit itk::Widget
26
27    constructor {args} { # defined below }
28    destructor { # defined below }
29
30    public method add {image {settings ""}}
31    public method get {}
32    public method delete {args}
33    public method scale {args}
34    public method parameters {title args} { # do nothing }
35    public method download {option args}
36
37    protected method _rebuild {args}
38    protected method _top {what}
39    protected method _zoom {option args}
40    protected method _move {option args}
41
42    private variable _dispatcher "" ;# dispatcher for !events
43    private variable _dlist ""      ;# list of data objects
44    private variable _topmost ""    ;# topmost image in _dlist
45    private variable _max           ;# max size of all images
46    private variable _scale         ;# info related to zoom
47    private variable _image         ;# image buffers used for scaling
48}
49                                                                               
50itk::usual ImageResult {
51    keep -background -foreground -cursor -font
52}
53
54# ----------------------------------------------------------------------
55# CONSTRUCTOR
56# ----------------------------------------------------------------------
57itcl::body Rappture::ImageResult::constructor {args} {
58    Rappture::dispatcher _dispatcher
59    $_dispatcher register !rebuild
60    $_dispatcher dispatch $this !rebuild [itcl::code $this _rebuild]
61
62    array set _scale {
63        max 1.0
64        current 1.0
65        default 1
66        x 0
67        y 0
68    }
69
70    option add hull.width hull.height
71    pack propagate $itk_component(hull) no
72
73    Rappture::Panes $itk_interior.panes -sashwidth 1 -sashrelief solid -sashpadding 2
74    pack $itk_interior.panes -expand yes -fill both
75    set main [$itk_interior.panes pane 0]
76    $itk_interior.panes fraction 0 1
77
78    itk_component add controls {
79        frame $main.cntls
80    } {
81        usual
82        rename -background -controlbackground controlBackground Background
83    }
84    pack $itk_component(controls) -side right -fill y
85
86    itk_component add reset {
87        button $itk_component(controls).reset \
88            -borderwidth 1 -padx 1 -pady 1 \
89            -bitmap [Rappture::icon reset] \
90            -command [itcl::code $this _zoom reset]
91    } {
92        usual
93        ignore -borderwidth
94        rename -highlightbackground -controlbackground controlBackground Background
95    }
96    pack $itk_component(reset) -padx 4 -pady 4
97    Rappture::Tooltip::for $itk_component(reset) "Reset the view to the default zoom level"
98
99    itk_component add zoomin {
100        button $itk_component(controls).zin \
101            -borderwidth 1 -padx 1 -pady 1 \
102            -bitmap [Rappture::icon zoomin] \
103            -command [itcl::code $this _zoom in]
104    } {
105        usual
106        ignore -borderwidth
107        rename -highlightbackground -controlbackground controlBackground Background
108    }
109    pack $itk_component(zoomin) -padx 4 -pady 4
110    Rappture::Tooltip::for $itk_component(zoomin) "Zoom in"
111
112    itk_component add zoomout {
113        button $itk_component(controls).zout \
114            -borderwidth 1 -padx 1 -pady 1 \
115            -bitmap [Rappture::icon zoomout] \
116            -command [itcl::code $this _zoom out]
117    } {
118        usual
119        ignore -borderwidth
120        rename -highlightbackground -controlbackground controlBackground Background
121    }
122    pack $itk_component(zoomout) -padx 4 -pady 4
123    Rappture::Tooltip::for $itk_component(zoomout) "Zoom out"
124
125
126    set _image(zoom) [image create photo]
127    set _image(final) [image create photo]
128
129    itk_component add image {
130        label $main.image -image $_image(final) -anchor c
131    } {
132        keep -background -foreground -cursor -font
133    }
134    pack $itk_component(image) -expand yes -fill both
135
136    #
137    # Add bindings for resize/move
138    #
139    bind $itk_component(image) <Configure> \
140        [list $_dispatcher event -idle !rebuild resize 1]
141
142    bind $itk_component(image) <ButtonPress-1> \
143        [itcl::code $this _move click %x %y]
144    bind $itk_component(image) <B1-Motion> \
145        [itcl::code $this _move drag %x %y]
146    bind $itk_component(image) <ButtonRelease-1> \
147        [itcl::code $this _move release %x %y]
148
149    #
150    # Add area at the bottom for notes.
151    #
152    set notes [$itk_interior.panes insert end -fraction 0.15]
153    $itk_interior.panes visibility 1 off
154    Rappture::Scroller $notes.scr -xscrollmode auto -yscrollmode auto
155    pack $notes.scr -expand yes -fill both
156    itk_component add notes {
157        Rappture::HTMLviewer $notes.scr.html
158    }
159    $notes.scr contents $notes.scr.html
160
161    eval itk_initialize $args
162}
163
164# ----------------------------------------------------------------------
165# DESTRUCTOR
166# ----------------------------------------------------------------------
167itcl::body Rappture::ImageResult::destructor {} {
168    foreach name [array names _image] {
169        image delete $_image($name)
170    }
171}
172
173# ----------------------------------------------------------------------
174# USAGE: add <image> ?<settings>?
175#
176# Clients use this to add an image to the plot.  The optional <settings>
177# are used to configure the image.  Allowed settings are -color,
178# -brightness, -width, -linestyle and -raise.
179# ----------------------------------------------------------------------
180itcl::body Rappture::ImageResult::add {image {settings ""}} {
181    array set params {
182        -color auto
183        -brightness 0
184        -width 1
185        -raise 0
186        -linestyle solid
187        -description ""
188        -param ""
189    }
190    foreach {opt val} $settings {
191        if {![info exists params($opt)]} {
192            error "bad setting \"$opt\": should be [join [lsort [array names params]] {, }]"
193        }
194        set params($opt) $val
195    }
196
197    if {$params(-raise)} {
198        set _topmost $image
199        $_dispatcher event -idle !rebuild
200    }
201
202    set pos [lsearch -exact $image $_dlist]
203    if {$pos < 0} {
204        lappend _dlist $image
205        $_dispatcher event -idle !rebuild
206    }
207}
208
209# ----------------------------------------------------------------------
210# USAGE: get
211#
212# Clients use this to query the list of images being displayed, in
213# order from bottom to top of this result.
214# ----------------------------------------------------------------------
215itcl::body Rappture::ImageResult::get {} {
216    # put the dataobj list in order according to -raise options
217    set dlist $_dlist
218
219    set i [lsearch $_dlist $_topmost]
220    if {$i >= 0} {
221        set dlist [lreplace $dlist $i $i]
222        set dlist [linsert $dlist 0 $_topmost]
223    }
224    return $dlist
225}
226
227# ----------------------------------------------------------------------
228# USAGE: delete ?<image1> <image2> ...?
229#
230# Clients use this to delete an image from the plot.  If no images
231# are specified, then all images are deleted.
232# ----------------------------------------------------------------------
233itcl::body Rappture::ImageResult::delete {args} {
234    if {[llength $args] == 0} {
235        set args $_dlist
236    }
237
238    # delete all specified curves
239    set changed 0
240    foreach image $args {
241        set pos [lsearch -exact $_dlist $image]
242        if {$pos >= 0} {
243            set _dlist [lreplace $_dlist $pos $pos]
244            set changed 1
245
246            if {$image == $_topmost} {
247                set _topmost ""
248            }
249        }
250    }
251
252    # if anything changed, then rebuild the plot
253    if {$changed} {
254        $_dispatcher event -idle !rebuild
255    }
256}
257
258# ----------------------------------------------------------------------
259# USAGE: scale ?<image1> <image2> ...?
260#
261# Sets the default limits for the overall plot according to the
262# limits of the data for all of the given <image> objects.  This
263# accounts for all images--even those not showing on the screen.
264# Because of this, the limits are appropriate for all images as
265# the user scans through data in the ResultSet viewer.
266# ----------------------------------------------------------------------
267itcl::body Rappture::ImageResult::scale {args} {
268    set _max(w) 0
269    set _max(h) 0
270    foreach image $args {
271        set imh [$image tkimage]
272
273        set w [image width $imh]
274        if {$w > $_max(w)} { set _max(w) $w }
275
276        set h [image height $imh]
277        if {$h > $_max(h)} { set _max(h) $h }
278    }
279
280    # scale is unknown for now... scale later at next _rebuild
281    set _scale(max) "?"
282    set _scale(current) "?"
283
284    $_dispatcher event -idle !rebuild
285}
286
287# ----------------------------------------------------------------------
288# USAGE: download coming
289# USAGE: download controls <downloadCommand>
290# USAGE: download now
291#
292# Clients use this method to create a downloadable representation
293# of the plot.  Returns a list of the form {ext string}, where
294# "ext" is the file extension (indicating the type of data) and
295# "string" is the data itself.
296# ----------------------------------------------------------------------
297itcl::body Rappture::ImageResult::download {option args} {
298    switch $option {
299        coming {
300            # nothing to do
301        }
302        controls {
303            # no controls for this download yet
304            return ""
305        }
306        now {
307            set top [_top image]
308            if {$top == ""} {
309                return ""
310            }
311            # Get the image data (as base64) and decode it back to binary.
312            # This is better than writing to temporary files.  When we switch
313            # to the BLT picture image it won't be necessary to decode the
314            # image data.
315            set bytes [$top data -format "jpeg -quality 100"]
316            set bytes [Rappture::encoding::decode -as b64 $bytes]
317            return [list .jpg $bytes]
318        }
319        default {
320            error "bad option \"$option\": should be coming, controls, now"
321        }
322    }
323}
324
325# ----------------------------------------------------------------------
326# USAGE: _rebuild ?<eventData>...?
327#
328# Called automatically whenever something changes that affects the
329# data in the widget.  Clears any existing data and rebuilds the
330# widget to display new data.
331# ----------------------------------------------------------------------
332itcl::body Rappture::ImageResult::_rebuild {args} {
333    array set event $args
334    if {[info exists event(resize)] && $event(resize)} {
335        # window changed size -- recompute max scale below
336        set _scale(max) "?"
337    }
338
339    if {$_scale(max) == "?"} {
340        if {![_zoom rescale]} {
341            return
342        }
343    }
344    if {$_scale(current) == "?" || $_scale(default)} {
345        set _scale(current) $_scale(max)
346        set _scale(x) 0.5
347        set _scale(y) 0.5
348    }
349
350    set w [winfo width $itk_component(image)]
351    set h [winfo height $itk_component(image)]
352    set bg [$itk_component(image) cget -background]
353
354    set imh [_top image]
355    if {$imh != ""} {
356        set iw [image width $imh]
357        set ih [image height $imh]
358
359        if {$_scale(current) <= 1.0} {
360            #
361            # Scale the image up by creating a "zoom" image which
362            # is smaller than the current image.  Sample a small
363            # part of the original image by copying into the "zoom"
364            # image, then scale that part up to the full "view" area.
365            #
366            set wz [expr {round($w*$_scale(current))}]
367            set hz [expr {round($h*$_scale(current))}]
368            if {$wz > $iw} {
369                set wz $iw
370            }
371            if {$hz > $ih} {
372                set hz $ih
373            }
374
375            set sx [expr {round($_scale(x)*$_max(w)-0.5*$wz)}]
376            if {$sx+$wz > $iw} {
377                set sx [expr {$iw-$wz}]
378            }
379            if {$sx < 0} {
380                set sx 0
381            }
382
383            set sy [expr {round($_scale(y)*$_max(h)-0.5*$hz)}]
384            if {$sy+$hz > $ih} {
385                set sy [expr {$ih-$hz}]
386            }
387            if {$sy < 0} {
388                set sy 0
389            }
390
391            if {$wz > 1 && $hz > 1} {
392                $_image(zoom) configure -width $wz -height $hz
393                set wf [expr {round(double($wz)/$_scale(current))}]
394                set hf [expr {round(double($hz)/$_scale(current))}]
395                $_image(final) configure -width $wf -height $hf
396                $_image(zoom) copy $imh -from $sx $sy
397                blt::winop resample $_image(zoom) $_image(final) sinc
398            }
399        } else {
400            #
401            # Scale the image down by creating a "zoom" image which
402            # is smaller than the current image.  Resize the original
403            # image to the smaller size, then copy into the current
404            # view.
405            #
406            set wz [expr {round(double($iw)/$_scale(current))}]
407            set hz [expr {round(double($ih)/$_scale(current))}]
408            if {$wz > 1 && $hz > 1} {
409                $_image(zoom) configure -width $wz -height $hz
410                $_image(zoom) put $bg -to 0 0 $wz $hz
411                blt::winop resample $imh $_image(zoom) sinc
412
413                $_image(final) configure -width $wz -height $hz
414                $_image(final) copy $_image(zoom) -from 0 0
415            }
416        }
417    }
418
419    set note [_top note]
420    if {[string length $note] > 0} {
421        if {[regexp {^html://} $note]} {
422            set note [string range $note 7 end]
423        } else {
424            regexp {&} $note {\007} note
425            regexp {<} $note {\&lt;} note
426            regexp {>} $note {\&gt;} note
427            regexp {\007} $note {\&amp;} note
428            regexp "\n\n" $note {<br/>} note
429            set note "<html><body>$note</body></html>"
430        }
431        set notes [$itk_interior.panes pane 1]
432        $itk_component(notes) load $note -in [file join [_top tooldir] docs]
433        $itk_interior.panes visibility 1 on
434    } else {
435        $itk_interior.panes visibility 1 off
436    }
437}
438
439# ----------------------------------------------------------------------
440# USAGE: _top image|note|tooldir
441#
442# Used internally to get the topmost image currently being displayed.
443# ----------------------------------------------------------------------
444itcl::body Rappture::ImageResult::_top {option} {
445    set top $_topmost
446    if {"" == $top} {
447        set top [lindex $_dlist 0]
448    }
449    if {"" != $top} {
450        switch -- $option {
451            image   { return [$top tkimage] }
452            note    { return [$top hints note] }
453            tooldir { return [$top hints tooldir] }
454            default { error "bad option \"$option\": should be image, note, tooldir" }
455        }
456    }
457    return ""
458}
459
460# ----------------------------------------------------------------------
461# USAGE: _zoom reset
462# USAGE: _zoom in
463# USAGE: _zoom out
464#
465# Called automatically when the user clicks on one of the zoom
466# controls for this widget.  Changes the zoom for the current view.
467# ----------------------------------------------------------------------
468itcl::body Rappture::ImageResult::_zoom {option args} {
469    switch -- $option {
470        rescale {
471            # empty list? then reset w/h max size
472            if {[llength $_dlist] == 0} {
473                set _max(w) 0
474                set _max(h) 0
475                set _scale(max) 1.0
476            } else {
477                set w [winfo width $itk_component(image)]
478                set h [winfo height $itk_component(image)]
479                if {$w == 1 && $h == 1} {
480                    return 0
481                }
482
483                if {$w < $h} {
484                    if {$_max(w)/double($_max(h)) > $w/double($h)} {
485                        set _scale(max) [expr {$_max(w)/double($w)}]
486                    } else {
487                        set _scale(max) [expr {$_max(h)/double($h)}]
488                    }
489                } else {
490                    if {$_max(w)/double($_max(h)) < $w/double($h)} {
491                        set _scale(max) [expr {$_max(h)/double($h)}]
492                    } else {
493                        set _scale(max) [expr {$_max(w)/double($w)}]
494                    }
495                }
496            }
497            return 1
498        }
499        reset {
500            set _scale(current) $_scale(max)
501            set _scale(default) 1
502            set _scale(x) 0.5
503            set _scale(y) 0.5
504        }
505        in {
506            set _scale(current) [expr {$_scale(current)*0.8}]
507            set _scale(default) 0
508        }
509        out {
510            set w [winfo width $itk_component(image)]
511            set h [winfo height $itk_component(image)]
512            if {$_max(w)/$_scale(current) > $w
513                  || $_max(h)/$_scale(current) > $h} {
514                # must be room left to zoom -- zoom out, but not beyond max
515                set _scale(current) [expr {$_scale(current)*1.25}]
516                if {$_scale(current) > $_scale(max)} {
517                    set _scale(current) $_scale(max)
518                }
519            } else {
520                # no room left to zoom -- zoom out max
521                set _scale(current) $_scale(max)
522            }
523
524            # fix the center of view, in case it is now out of bounds
525            if {$_scale(current) > 1.0} {
526                set _scale(x) 0.5
527                set _scale(y) 0.5
528            }
529
530            # keep this zoom setting now that we've zoomed out
531            set _scale(default) 0
532        }
533    }
534    $_dispatcher event -idle !rebuild
535}
536
537# ----------------------------------------------------------------------
538# USAGE: _move click <x> <y>
539# USAGE: _move drag <x> <y>
540# USAGE: _move release <x> <y>
541#
542# Called automatically when the user clicks and drags on the image
543# to pan the view.  Adjusts the (x,y) offset for the scaling info
544# and redraws the widget.
545# ----------------------------------------------------------------------
546itcl::body Rappture::ImageResult::_move {option args} {
547    switch -- $option {
548        click {
549            foreach {x y} $args break
550            $itk_component(image) configure -cursor fleur
551            set _scale(x0) $_scale(x)
552            set _scale(y0) $_scale(y)
553            set _scale(xclick) $x
554            set _scale(yclick) $y
555        }
556        drag {
557            foreach {x y} $args break
558            if {[info exists _scale(xclick)] && [info exists _scale(yclick)]} {
559                set w [winfo width $itk_component(image)]
560                set h [winfo height $itk_component(image)]
561                set wx [expr {round($_max(w)/$_scale(current))}]
562                set hy [expr {round($_max(h)/$_scale(current))}]
563                if {$wx > $w || $hy > $h} {
564                    set x [expr {$_scale(x0)-($x-$_scale(xclick))/double($wx)}]
565                    set y [expr {$_scale(y0)-($y-$_scale(yclick))/double($hy)}]
566                    if {$x*$_max(w) < 0.5*$w*$_scale(current)} {
567                        set x [expr {0.5*$w*$_scale(current)/$_max(w)}]
568                    }
569                    if {$x*$_max(w) > $_max(w) - 0.5*$w*$_scale(current)} {
570                        set x [expr {1 - 0.5*$w*$_scale(current)/$_max(w)}]
571                    }
572                    if {$y*$_max(h) < 0.5*$h*$_scale(current)} {
573                        set y [expr {0.5*$h*$_scale(current)/$_max(h)}]
574                    }
575                    if {$y*$_max(h) > $_max(h) - 0.5*$h*$_scale(current)} {
576                        set y [expr {1 - 0.5*$h*$_scale(current)/$_max(h)}]
577                    }
578                    set _scale(x) $x
579                    set _scale(y) $y
580                } else {
581                    set _scale(x) 0.5
582                    set _scale(y) 0.5
583                }
584                $_dispatcher event -idle !rebuild
585            }
586        }
587        release {
588            eval _move drag $args
589            $itk_component(image) configure -cursor ""
590            catch {unset _scale(xclick)}
591            catch {unset _scale(yclick)}
592        }
593        default {
594            error "bad option \"$option\": should be click, drag, release"
595        }
596    }
597}
Note: See TracBrowser for help on using the repository browser.