001package votorola.s.gwt.stage.vote; // Copyright 2012-2013, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.
002
003import com.google.gwt.core.client.*;
004import com.google.gwt.dom.client.*;
005import com.google.gwt.uibinder.client.*;
006import com.google.gwt.user.client.Timer;
007import com.google.gwt.user.client.ui.*;
008import com.google.web.bindery.event.shared.HandlerRegistration;
009import java.util.*;
010import org.vectomatic.dom.svg.*;
011import org.vectomatic.dom.svg.utils.OMSVGParser;
012import votorola.a.count.gwt.*;
013import votorola.g.hold.*;
014import votorola.g.lang.*;
015import votorola.g.web.gwt.*;
016import votorola.g.web.gwt.event.*;
017import votorola.s.gwt.stage.*;
018
019import static votorola.a.count.XCastRelation.VOTER;
020import static votorola.a.count.XCastRelation.CO_VOTER;
021
022
023/** A view of a {@linkplain VoteTrack vote track}.  It shows the anchor node's {@linkplain
024  * VoteTrack#voters() voters} left, {@linkplain VoteTrack#peers() peers} center, and
025  * candidate right.  The anchor node itself may have a {@linkplain BoardV pin indicator}.
026  * Vote flow is depicted left to right with the volume indicated by numbers.<pre>
027  *
028  *         votersV                    peersV            candidateV
029  *             \                        |                  /
030  *    >>>>>>>>>>>>>>>>>>>>> --- >>>>>>>>>>>>>>>>>>>> --- >>>>>
031  *                           |              /\        |
032  *                      flow volume         /    flow volume
033  *                                        pin</pre>
034  *
035  * <p>When the track is invisible, e.g. because no poll is staged, then a {@linkplain
036  * Podium podium} may become visible to serve as a staging control for the actor.</p><pre
037  *
038 *>                                            ( )
039  *                                              \
040  *                                             podium</pre>
041  *
042  * <p id='ackC'>Acknowledgement: The segmented arrow motif is borrowed from Christian
043  * Weilbach's design of a progress track for quantitative summation accounts.  See his
044  * mock-ups <a href='http://mail.zelea.com/list/votorola/2011-September/001181.html'
045  * target='_top'>2b and 2g</a>.</p>
046  *
047  *     @see <a href='http://reluk.ca/project/votorola/s/gwt/stage/_/mock/'
048  *       target='_top'>Guiding mock-ups</a>
049  *     @see <a href='../../../../../../../s/gwt/stage/vote/VoteTrackV.ui.xml'
050  *                                                        >VoteTrackV.ui.xml</a>
051  */
052public final class VoteTrackV extends Composite
053{
054
055
056    /** Constructs a VoteTrackV.
057      *
058      *     @param isBottomFixed answers whether this VoteTrackV is to be the {@linkplain
059      *       #iBottomFixed bottom-fixed instance}.  You may create at most one
060      *       bottom-fixed instance.
061      */
062    public VoteTrackV( VoteTrack _track, final boolean isBottomFixed )
063    {
064        track = _track;
065        if( isBottomFixed )
066        {
067            if( iBottomFixed != null ) throw new IllegalArgumentException( "duplicate instance" );
068
069            iBottomFixed = VoteTrackV.this;
070        }
071        final UiBinderI uiBinder = GWT.create( UiBinderI.class );
072        initWidget( uiBinder.createAndBindUi( VoteTrackV.this ));
073        if( DifferenceLight.sceneName() != null ) addStyleName( "diffScene" );
074        if( DartLight.i() == null ) new DartLight();
075
076      // Main components.
077      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
078        initB( votersBoardV = new BoardV( track.voters(), VoteTrackV.this, vBoardElement, VOTER ));
079        initB( peersBoardV = new PeerBoardV( track.peers(), VoteTrackV.this, pBoardElement ));
080        init( candidateV = new CandidateV( VoteTrackV.this, cElement ));
081        init( new FlowVolumeV( track.anchorHolder(), VoteTrackV.this, vOutflowElement, VOTER ));
082        init( new FlowVolumeV( track.candidateHolder(), VoteTrackV.this, pOutflowElement, CO_VOTER));
083
084      // Overlay.
085      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
086        final OMSVGSVGElement svg = OMSVGParser.createDocument().createSVGSVGElement();
087        overlayDiv.appendChild( svg.getElement() );
088        svg.appendChild( new Podium( peersBoardV ));
089    }
090
091
092
093    /** The {@linkplain StageV staged instance} of VoteTrackV, or null if none is staged.
094      */
095    public static VoteTrackV i( final StageV stageV )
096    {
097        final Iterator<Widget> ww = stageV.iterator();
098        while( ww.hasNext() )
099        {
100            final Widget w = ww.next();
101            if( w instanceof VoteTrackV ) return (VoteTrackV)w;
102        }
103
104        return null;
105    }
106
107
108
109    /** The instance of VoteTrackV that is fixed to the bottom of the viewport, or null if
110      * there is none.
111      */
112    static VoteTrackV iBottomFixed() { return iBottomFixed; }
113
114
115        private static VoteTrackV iBottomFixed;
116
117
118
119    private void init( final MajorV child )
120    {
121        // A problem with UIBinder (2.4) is the reason for adding these child widgets by
122        // their elements.  UIBinder apparently cannot substitute widgets for table
123        // elements in an HTMLPanel.  Explicitly specifying <BoardV> (for example) in the
124        // template instead of <td> results in a TypeError from JavaScript: "[object
125        // DOMWindow] has no method 'replaceChild'" (Chrome) or "this.replaceChild is not
126        // a function" (FireFox).
127        final HTMLPanel y = getWidget();
128        y.add( child, /*parent to add to*/child.getElement().getParentElement() );
129          // element already attached to that parent obviously, but there's no
130          // addWidgetOnly method.  addAndReplaceElement used to work till 2.5, now it
131          // silently rejects the widget if its element == element to replace
132    }
133
134
135
136    private void initB( final BoardV child )
137    {
138        child.init();
139        init( child );
140    }
141
142
143
144    @Warning("non-API") interface UiBinderI extends UiBinder<HTMLPanel,VoteTrackV> {}
145
146
147
148   // ` e a r l y ````````````````````````````````````````````````````````````````````````
149
150
151    private final Spool spool = new Spool1();
152
153
154
155   // ------------------------------------------------------------------------------------
156
157
158    /** The view of the {@linkplain VoteTrack#candidate candidate}.
159      */
160    public CandidateV candidateV() { return candidateV; }
161
162
163        private final CandidateV candidateV;
164
165
166        @UiField @Warning("non-API") TableCellElement cElement;
167
168
169
170    /** Answers whether this view is physically moving in order to visualize a change of
171      * anchor and a consequent logical shift in votespace.  The return value is bound to
172      * property name <tt>moving</tt>.  Components themselves ought to withold showing a
173      * change of anchor or related changes till immediately after the motion has stopped.
174      * Each major component must also explicitly {@linkplain MajorV#setVisible(boolean)
175      * reshow} itself after the changes are rendered, as this view may have hidden it at
176      * some point as part of the motion.
177      */
178    public boolean isMoving() { return animation != null; };
179
180
181        private Animation animation;
182
183
184        private void setMoving( final Animation newAnimation )
185        {
186            if( animation != null || newAnimation == null )
187            {
188                assert false;
189                return;
190            }
191
192            animation = newAnimation;
193            fireEvent( new PropertyChange( "moving" ));
194        }
195
196
197        private void clearMoving()
198        {
199            if( animation == null )
200            {
201                assert false;
202                return;
203            }
204
205            animation = null;
206            fireEvent( new PropertyChange( "moving" ));
207        }
208
209
210
211    /** The view of the {@linkplain VoteTrack#peers peers board}.
212      */
213    public PeerBoardV peersBoardV() { return peersBoardV; }
214
215
216        private final PeerBoardV peersBoardV;
217
218
219        @UiField @Warning("non-API") TableCellElement pElement;
220
221
222        @UiField @Warning("non-API") TableCellElement pBoardElement;
223
224
225        @UiField @Warning("non-API") TableCellElement pOutflowElement;
226
227
228
229    /** The track on which this view is modelled.
230      */
231    public VoteTrack track() { return track; }
232
233
234        private final VoteTrack track;
235
236
237
238    /** The view of the {@linkplain VoteTrack#voters voters board}.
239      */
240    public BoardV votersBoardV() { return votersBoardV; }
241
242
243        private final BoardV votersBoardV;
244
245
246        @UiField @Warning("non-API") TableCellElement vElement;
247
248
249        @UiField @Warning("non-API") TableCellElement vBoardElement;
250
251
252        @UiField @Warning("non-API") TableCellElement vOutflowElement;
253
254
255
256   // - C o m p o s i t e ----------------------------------------------------------------
257
258
259    protected @Override HTMLPanel getWidget() { return (HTMLPanel)super.getWidget(); }
260
261
262
263   // - W i d g e t ----------------------------------------------------------------------
264
265
266    protected @Override void onLoad()
267    {
268        if( !spool.isUnwinding() )
269        {
270            Stage.i().addInitializer( new TheatreInitializerC() // auto-removed
271            {
272                public void initComplete( final Stage stage, boolean _rPending )
273                {
274                    if( spool.isUnwinding() ) return;
275
276                    // assume here (for sake of simplicity) that default will not change:
277                    if( !VoteTrack.toPin(stage) ) new Animator( stage );
278                }
279            });
280        }
281        super.onLoad();
282    }
283
284
285
286    protected @Override void onUnload()
287    {
288        super.onUnload();
289        spool.unwind();
290    }
291
292
293
294    public @Override void removeFromParent()
295    {
296        if( getParent() != null ) throw new UnsupportedOperationException();
297    }
298
299
300
301//// P r i v a t e ///////////////////////////////////////////////////////////////////////
302
303
304    private static DartLight dartLight;
305
306
307
308    @UiFactory @Warning("non-API") HeadsUpDisplay newHeadsUpDisplay()
309    {
310        return new HeadsUpDisplay( VoteTrackV.this, spool );
311    }
312
313
314
315    @UiField @Warning("non-API") DivElement overlayDiv;
316
317
318
319    @UiField @Warning("non-API") TableElement tableElement; // outermost table
320
321
322
323   // ====================================================================================
324
325
326    private final class Animation extends Timer // OPT instead use AnimationScheduler
327    {
328
329        Animation( int _direction )
330        {
331            direction = _direction;
332            setMoving( Animation.this );
333            msStart = System.currentTimeMillis();
334            rate_percentPerMS = /*td.voters width per vote/track.css*/47d / MS_DURATION * direction;
335            schedule( MS_PERIOD / 2 ); /* for first frame, which has faster start time for
336              sake of accuracy in face of granular timing */
337        }
338
339
340        private final int direction; // of track motion, -1 or 1
341
342
343        private Runnable frame = new Runnable()
344        {
345            public void run() // first frame only
346            {
347                if( direction == -1 ) candidateV.setVisible( false );
348                  // size and position inaccurate, eagerly hide on leftward slide
349                frame = new Runnable()
350                {
351                    public void run() // all subsequent frames, and also called into first frame
352                    {
353                        final int msLapse = (int)(System.currentTimeMillis() - msStart);
354                        if( msLapse >= MS_STOP )
355                        {
356                            move( MS_DURATION ); /* do not stop part way even if msLapse
357                              is less because final frame may hold for longer than others,
358                              calling attention to its misplacement */
359                            stop( /*isImmediate*/false );
360                        }
361                        else move( msLapse );
362                    }
363                };
364                frame.run();
365                if( !isCancelled ) scheduleRepeating( MS_PERIOD ); // for subsequent frames
366            }
367        };
368
369
370        private boolean isCancelled;
371
372
373        private boolean isStopped;
374
375
376        private void move( final int msLapse )
377        {
378            final double distance = msLapse * rate_percentPerMS;
379            tableStyle.setLeft( distance, Style.Unit.PCT );
380        }
381
382
383        private static final int MS_DURATION = 200;
384
385
386        private static final int MS_PERIOD = 25; // needn't be especially smooth
387
388
389        private static final int MS_STOP = MS_DURATION - MS_PERIOD / 2;
390          // faster end time, more accurate, correcting for granularity
391
392
393        private final long msStart;
394
395
396        private final double rate_percentPerMS; // speed of motion
397
398
399        void stop( final boolean isImmediate ) /* "stop" rather than "cancel" because
400          cancel() is called by superclass on scheduling before the first tick */
401        {
402            cancel();
403            isCancelled = true;
404            if( isImmediate ) stop2();
405            else Scheduler.get().scheduleDeferred( new Scheduler.ScheduledCommand()
406            {
407                public void execute() /* after browser renders the previous motion.  This
408                  delay tends to fail in dev mode, making the end of the animation jumpy;
409                  but it works in production mode */
410                {
411                  // Hide components till they re-render correctly.
412                  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
413                    for( final Widget component: getWidget() )
414                    {
415                        // before clearing motion and restoring original left position of
416                        // view, hide each component till it finishes re-rendering in
417                        // response to clearMoving.  See isMoving
418                        if( component instanceof MajorV ) component.setVisible( false );
419                    }
420
421                  // Fade in components that show new information.
422                  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
423                    final ArrayList<Element> fades = new ArrayList<Element>( /*initial capacity*/2 );
424                    if( direction == 1 ) fades.add( vElement );
425                    else
426                    {
427                        fades.add( pElement );
428                        fades.add( cElement );
429                    }
430                    for( Element e: fades ) e.addClassName( "fadedOut" ); // begin faded out
431                    Scheduler.get().scheduleDeferred( new Scheduler.ScheduledCommand()
432                    {
433                        public void execute() // after browser renders fadedOut
434                        {
435                            for( Element e: fades ) e.removeClassName( "fadedOut" ); // fade in
436                        }
437                    });
438
439                  // - - -
440                    stop2();
441                }
442            });
443
444        }
445
446
447        private void stop2()
448        {
449            isStopped = true;
450            tableStyle.clearLeft();
451            clearMoving();
452        }
453
454
455        private final Style tableStyle = tableElement.getStyle();
456
457
458       // - T i m e r --------------------------------------------------------------------
459
460
461        public void run()
462        {
463            if( isStopped || spool.isUnwinding() ) return;
464
465            assert animation == Animation.this;
466            frame.run();
467        }
468
469    }
470
471
472
473   // ====================================================================================
474
475
476    private final class Animator implements ChangeHandler, PropertyChangeHandler
477    {
478
479        Animator( final Stage stage )
480        {
481         // assert !VoteTrack.toPin(stage): "animator not enabled for pinned track";
482         //   // else "animate" style hides overflow, and BoardV.pinImg won't quite render
483         /// no longer a problem with div.animationBed
484            addStyleName( "animate" );
485            spool.add( new Hold()
486            {
487                final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource(
488                  Change.TYPE, /*source*/track, Animator.this );
489                public void release() { hR.removeHandler(); }
490            });
491            spool.add( new Hold()
492            {
493                final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource(
494                  PropertyChange.TYPE, /*source*/stage, Animator.this );
495                public void release() { hR.removeHandler(); }
496            });
497            spool.add( new Hold()
498            {
499                public void release() { if( animation != null )
500                  animation.cancel(); }
501            });
502            anchorLast = track.anchor();
503        }
504
505
506        private CountNodeJS anchorLast;
507
508
509        public void onChange( Change _e )
510        {
511            if( spool.isUnwinding() ) return;
512
513            final CountNodeJS anchor = track.anchor();
514            anchorLast = anchor;
515        }
516
517
518        public void onPropertyChange( final PropertyChange e )
519        {
520            // Motion is here triggered by change events that issue immediately from the
521            // stage.  Component views learn of these events somewhat later because of the
522            // delayed intermediation of VoteTrack.gun.  Motion will have commenced by
523            // then and the component views will therefore postpone their remodelling till
524            // after the motion stops.  See isMoving().
525
526            if( !"actorName".equals(e.propertyName()) || spool.isUnwinding() ) return;
527
528            if( animation != null )
529            {
530                animation.stop( /*isImmediate*/true ); // actor changed in mid-motion, abort motion
531                return; // do not try to re-animate, just do an immediate transition
532            }
533
534            if( anchorLast == null  ) return;
535
536            final String anchorName = VoteTrack.anchorName( Stage.i() );
537            if( anchorName == null ) return;
538
539            if( anchorName.equals( anchorLast.candidateName() )) new Animation( /*stage left*/-1 );
540            else if( CountNodeJS.findNode(anchorName,anchorLast.voters()) != null )
541            {
542                new Animation( /*stage right*/1 );
543            }
544        }
545
546
547    }
548
549
550}