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.Scheduler; 004import com.google.gwt.dom.client.*; 005import com.google.gwt.user.client.ui.*; 006import com.google.web.bindery.event.shared.HandlerRegistration; 007import java.util.*; 008import org.vectomatic.dom.svg.*; 009import org.vectomatic.dom.svg.utils.*; 010import votorola.a.count.*; 011import votorola.a.count.gwt.*; 012import votorola.g.hold.*; 013import votorola.g.lang.*; 014import votorola.g.web.gwt.*; 015import votorola.g.web.gwt.event.*; 016import votorola.s.gwt.stage.*; 017 018import static votorola.a.count.CountNode.DART_SECTOR_MAX; 019 020 021/** A view of a board rendered as an HTML table cell containing an SVG drawing. The 022 * drawing is visible only when the board is enabled. It comprises a single {@linkplain 023 * NodeV node view} for the {@linkplain Board#mosquito() mosquito} (0) followed by 024 * {@value votorola.a.count.CountNode#DART_SECTOR_MAX} node views for the {@linkplain 025 * Board#node(int) dart sectored nodes}. The length of each node view is roughly 026 * proportional to its vote flow. Unoccupied nodes are indicated by subdued 027 * styling.<pre> 028 * 029 * +---+---+--------+---+- - -+---+----------------+---+---+---+ 030 * \ \ \ \ \ \ \ \ \ \ \ 031 * + + + + + + + + + + + 032 * / / / / / / / / / / / 033 * +---+---+--------+---+- - -+---+----------------+---+---+---+ 034 * 0 1 2 3 . . . 16 18 19 20</pre> 035 */ 036public class BoardV extends NodeV.Box 037{ 038 039 040 /** Partially constructs a BoardV for {@linkplain #init() init} to finish. 041 * 042 * @param _board the board on which the view is modelled. 043 * @param element the outermost HTML element of which the view is composed. 044 */ 045 BoardV( Board _board, final VoteTrackV trackV, final TableCellElement element, 046 XCastRelation _place ) 047 { 048 super( _place, trackV ); 049 board = _board; 050 051 // HTML view. 052 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 053 setElement( element ); 054 055 // SVG view. 056 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 057 final OMSVGDocument svgDoc = OMSVGParser.createDocument(); 058 svg = svgDoc.createSVGSVGElement(); 059 element.appendChild( svg.getElement() ); 060 nodes = svgDoc.createSVGGElement(); 061 svg.appendChild( nodes ); 062 final VoteTrack track = trackV.track(); 063 for( int s = 0; s < MAX_NODE_COUNT; ++s ) 064 { 065 nodes.appendChild( new NodeV( BoardV.this, s, track )); 066 } 067 068 // Controllers. 069 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 070 new Modeller( trackV ); 071 } 072 073 074 075 /** Completes the construction of this BoardV. Call once only. 076 */ 077 void init() { init( new Painter() ); } 078 079 080 081 /** Completes the construction of this BoardV using a particular painter. Call once 082 * only. 083 * 084 * @see #painter() 085 */ 086 void init( Painter _painter ) { painter = _painter; } 087 088 089 090 // - C o u n t - N o d e . B o x ------------------------------------------------------ 091 092 093 public final Iterable<NodeV> nodeViews() { return nodeViews; } 094 095 096 private final Iterable<NodeV> nodeViews = new Iterable<NodeV>() 097 { 098 public Iterator<NodeV> iterator() 099 { 100 return new votorola.g.util.IteratorA<NodeV>() 101 { 102 private NodeV nextV = (NodeV)nodes.getFirstChild(); 103 104 public boolean hasNext() { return nextV != null; } 105 106 public NodeV next() 107 { 108 if( nextV == null ) throw new NoSuchElementException(); 109 110 final NodeV nodeV = nextV; 111 nextV = nodeV.getNextSibling(); 112 return nodeV; 113 } 114 }; 115 } 116 }; 117 118 119 120 /** The painter for this board. 121 */ 122 public final Painter painter() { return painter; } 123 124 125 private Painter painter; // final after init 126 127 128 129 /** The SVG component of this view. 130 */ 131 final OMSVGSVGElement svg() { return svg; } 132 133 134 private final OMSVGSVGElement svg; 135 // could not instead bind as "@UiField OMSVGSVGElement" because Element "is not 136 // assignable to" OMSVGSVGElement 137 138 139 140 // - W i d g e t ---------------------------------------------------------------------- 141 142 143 protected @Override void onLoad() 144 { 145 if( !spool().isUnwinding() ) painter.init(); 146 // after load, when size known [but unclear how init depends on size] 147 super.onLoad(); 148 } 149 150 151 152 // ==================================================================================== 153 154 155 /** A painter for a board. 156 */ 157 class Painter extends NodeVPainter 158 { 159 160 Painter() { super( BoardV.this ); } 161 162 163 private void init() 164 { 165 init( spool() ); 166 spool().add( new Hold() 167 { 168 final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( 169 PropertyChange.TYPE, /*source*/Stage.i(), Painter.this ); 170 public void release() { hR.removeHandler(); } 171 }); 172 repaint(); 173 } 174 175 176 // -------------------------------------------------------------------------------- 177 178 179 /** Repaints the view. 180 */ 181 final @Override void repaint( final int width, final float protrusion, final int y, 182 final float halfThickness ) 183 { 184 if( !board.isEnabled() ) return; // await call from remodel 185 186 palette.recalibrate( width, protrusion, board.isEndBoard() ); 187 // int s = 0; // dart sector, starts at mosquito (0) 188 // final int sAnchor; 189 // { 190 // final CountNodeJS anchor = board.track().anchor(); 191 // sAnchor = anchor == null? -1: anchor.dartSector(); 192 // } 193 /// anchor node null till voters fetched, so use anchor *name*: 194 final String anchorName = VoteTrack.anchorName( Stage.i() ); 195 float x = 0; 196 float xAnchor = -1; // unless found 197 float lengthAnchor = -1; 198 for( NodeV child = (NodeV)nodes.getFirstChild(); child != null; 199 child = child.getNextSibling() ) 200 { 201 final CountNodeJS node = child.getCountNode(); 202 final float length = palette.calculateLength( node ); 203 child.repaint( x, length, protrusion, y, halfThickness ); 204 // if( s == sAnchor ) 205 if( node != null && node.name().equals( anchorName )) 206 { 207 xAnchor = x; 208 lengthAnchor = length; 209 } 210 // ++s; 211 x += length; 212 x += MARGIN_INTER; 213 } 214 repaint2( protrusion, y, halfThickness, xAnchor, lengthAnchor ); 215 GWTX.i().bus().fireEventFromSource( new Change(), Painter.this ); 216 } 217 218 219 /** Repaints extended parts of the view. The implementation of this method in the 220 * base class Painter does nothing. 221 */ 222 void repaint2( float _protrusion, int _y, float _halfThickness, float _xAnchor, 223 float _lengthAnchor ) {} 224 225 226 // - P r o p e r t y - C h a n g e - H a n d l e r -------------------------------- 227 228 229 public final @Override void onPropertyChange( final PropertyChange e ) 230 { 231 if( spool().isUnwinding() ) return; 232 233 super.onPropertyChange( e ); 234 if( e.getSource() == Stage.i() && e.propertyName().equals("differencesShadowed") ) 235 { 236 repaint(); 237 } 238 } 239 240 } 241 242 243 244 // ==================================================================================== 245 246 247 /** A workspace for intermediate calculations in painting a board. 248 */ 249 static final class Palette 250 { 251 252 253 private float apportionableWidth; 254 255 256 257 private LengthApportioner apportioner; 258 259 260 261 /** Calculates the length of the node view for the specified node. 262 */ 263 float calculateLength( final CountNodeJS node ) 264 { 265 final float portion = apportioner.calculatePortion( node ); 266 return minLength + portion * apportionableWidth; 267 } 268 269 270 271 private final LengthApportioner outflowApportioner = new LengthApportioner() 272 { 273 float calculatePortion( final CountNodeJS node ) 274 { 275 final float portion; 276 if( node == null ) portion = 0; 277 else 278 { 279 final SacRegisterJS_v reg = node.voteRegister(); 280 portion = ((float)reg.carryVolume() + (float)reg.castVolume()) / outflowTotal; 281 } 282 return portion; 283 } 284 }; 285 286 287 288 /** The total of cast and carried volume for all nodes in the board. 289 */ 290 long outflowTotal; 291 292 293 294 private float minLength; 295 296 297 298 private final LengthApportioner occupantApportioner = new LengthApportioner() 299 { 300 float calculatePortion( final CountNodeJS node ) 301 { 302 final int weightTotal = MAX_NODE_COUNT + occupantCount; 303 final int weight; 304 if( node == null ) weight = 1; 305 else weight = 2; 306 return (float)weight / weightTotal; 307 } 308 }; 309 310 311 312 /** The count of non-null nodes in the board. 313 */ 314 int occupantCount; 315 316 317 318 /** Recalibrates to new board dimensions. 319 * 320 * @param width the rendered width of the {@linkplain 321 * NodeVPainter#container HTML container} of the node views. 322 */ 323 void recalibrate( final int width, final float protrusion, final boolean isEndBoard ) 324 { 325 final float lengthSum = width - protrusion - (MAX_NODE_COUNT-1) * MARGIN_INTER; 326 // total of width actually occupied by nodes 327 apportionableWidth = lengthSum - MAX_NODE_COUNT * PREFERRED_MIN_LENGTH; 328 // amount of lengthSum to be apportioned based on received volume, such that 329 // nodes with greater volume are rendered in greater length 330 if( apportionableWidth < 0 ) 331 { 332 minLength = lengthSum / MAX_NODE_COUNT; 333 apportionableWidth = 0; 334 } 335 else minLength = PREFERRED_MIN_LENGTH; 336 if( isEndBoard ) apportioner = receiveVolumeApportioner; 337 else if( outflowTotal > 0 ) apportioner = outflowApportioner; 338 else apportioner = occupantApportioner; // probably a lone mosquito on board 339 } 340 341 342 343 private final LengthApportioner receiveVolumeApportioner = new LengthApportioner() 344 { 345 float calculatePortion( final CountNodeJS node ) 346 { 347 final float portion; 348 if( node == null ) portion = 0; 349 else portion = (float)node.voteRegister().receiveVolume() / receiveTotal; 350 return portion; 351 } 352 }; 353 354 355 356 /** The total of received volume for all nodes in the board. 357 */ 358 long receiveTotal; 359 360 361 } 362 363 364 365//// P r i v a t e /////////////////////////////////////////////////////////////////////// 366 367 368 private final Board board; 369 370 371 372 private static final int MARGIN_INTER = NodeV.STROKE_WIDTH > 0? 0: 3; 373 // angled as it is, the 2 pixel stroke alone is roughly equivalent to a 3 pixel margin 374 375 376 377 private static final int MAX_NODE_COUNT = DART_SECTOR_MAX + 1; 378 379 380 381 private final OMSVGGElement nodes; 382 383 384 385 private final Palette palette = new Palette(); 386 387 388 389 private static final int PREFERRED_MIN_LENGTH = 7/*apparent*/ + NodeV.STROKE_WIDTH; 390 391 392 393 // ==================================================================================== 394 395 396 private static abstract class LengthApportioner 397 { 398 abstract float calculatePortion( CountNodeJS node ); 399 } 400 401 402 403 // ==================================================================================== 404 405 406 private final class Modeller extends SuspendedModeller 407 { 408 409 Modeller( final VoteTrackV trackV ) 410 { 411 super( trackV, spool() ); 412 remodelUnlessMoving(); // init state 413 } 414 415 416 final @Warning("init call") void remodel() 417 { 418 if( board.isEnabled() ) 419 { 420 palette.occupantCount = 0; 421 palette.outflowTotal = 0; 422 palette.receiveTotal = 0; 423 wasChanged = false; 424 // for( final NodeV v: nodeViews() ) remodel( v, board.node(v.dartSector()) ); 425 /// cannot fetch mosquito thus from board, so: 426 NodeV v = (NodeV)nodes.getFirstChild(); 427 remodel( v, board.mosquito() ); 428 for( int s = 1; s <= DART_SECTOR_MAX; ++s ) 429 { 430 v = v.getNextSibling(); 431 remodel( v, board.node(s) ); 432 } 433 if( palette.occupantCount > 0 ) 434 { 435 setVisible( true ); 436 if( painter == null || !wasChanged ) return; // save expensive repaint 437 438 Scheduler.get().scheduleFinally( new Scheduler.ScheduledCommand() 439 { 440 public void execute() { 441 painter.repaint(); } 442 // after browser's layout engine responds to changes above 443 }); 444 return; 445 } 446 // else probably a voters board in which there are no actual voters 447 } 448 449 setVisible( false ); 450 } 451 452 453 private final @Warning("init call") void remodel( final NodeV child, 454 final CountNodeJS node ) 455 { 456 if( child.setCountNode( node )) wasChanged = true; 457 if( node == null ) return; 458 459 ++palette.occupantCount; 460 final SacRegisterJS_v reg = node.voteRegister(); 461 palette.outflowTotal += reg.carryVolume() + reg.castVolume(); 462 palette.receiveTotal += reg.receiveVolume(); 463 } 464 465 466 private boolean wasChanged; 467 468 } 469 470 471}