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}