001package votorola.s.gwt.scene.vote; // Copyright 2011, 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.Element; 005import com.google.gwt.event.dom.client.*; 006import com.google.gwt.user.client.Window; 007import com.google.gwt.user.client.ui.HTML; 008import com.google.gwt.user.client.ui.RootPanel; 009import com.google.gwt.view.client.*; 010import com.google.web.bindery.event.shared.HandlerRegistration; 011import org.vectomatic.dom.svg.*; 012import org.vectomatic.dom.svg.utils.*; 013import votorola.a.count.gwt.*; 014import votorola.a.web.gwt.*; 015import votorola.g.hold.*; 016import votorola.g.lang.*; 017import votorola.g.web.gwt.*; 018import votorola.g.web.gwt.event.*; 019import votorola.g.web.gwt.svg.*; 020import votorola.s.gwt.scene.*; 021import votorola.s.gwt.stage.*; 022import votorola.s.gwt.stage.link.*; 023 024import static org.vectomatic.dom.svg.OMSVGLength.SVG_LENGTHTYPE_PX; 025 026 027/** A view of a {@linkplain Votespace votespace} in which the main component is 028 * implemented as a scalable vector graphic. The overall layout of the page is as 029 * follows:<pre> 030 * 031 * +--------------------------------------------------------+ 032 * | stage | 033 * +-------------+------------------------------------------+ 034 * | | | 035 * | | | 036 * | | | 037 * | | | 038 * | | | 039 * | feed | | 040 * | | svg | 041 * | | | 042 * | | | 043 * | | | 044 * | | | 045 * | | | 046 * +-------------+------------------------------------------+</pre> 047 * 048 * <p>Except where noted otherwise in the API, lengths for the vector graphic (svg) are 049 * given in "em" units. Angles are given in degrees measured clockwise from the positive 050 * (right hand) x axis.</p> 051 * 052 * <p>Acknowledgement: The design of this view follows from the suggestions of Thomas von 053 * der Elbe, in a Skype discussion on April 11, 2011. See the Metagovernment <a 054 * href='http://reluk.ca/var/cache/irc/metagov/11-04/11' target='_top'>IRC log</a> and 055 * the <a 056 * href='http://metagovernment.org/pipermail/start_metagovernment.org/2011-April/003819.html' 057 * target='_top'>follow up post</a> to the Metagovernment mailing list.</p> 058 * 059 * @see votorola.s.gwt.stage.StageIn 060 * @see <a href='http://reluk.ca/y/vw/xf/#c=DV&s=G!p!sandbox' target='_top'>Live 061 * example of a VotespaceV (right)</a> 062 */ 063 @Warning("permanently disabled on detach from document") 064public final class VotespaceV extends HTML implements SVGNest<SVGNest<?>> 065{ 066 067 // Note: Intermediate calculations are done in floats only because the SVG library 068 // dislikes doubles. 069 070 071 /** Constructs a VotespaceV. 072 */ 073 public VotespaceV( Votespace _model ) 074 { 075 model = _model; 076 077 final Element y = getElement(); 078 y.addClassName( "vote-VotespaceV" ); 079 080 // Resource accounting view. 081 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 082 if( Scenes.toUseRAC() ) 083 { 084 final LinkTrackV linkTrackV = LinkTrackV.i( StageV.i() ); 085 if( linkTrackV != null ) 086 { 087 final SacSelectionV sacSelectionV = new SacSelectionV( model ); 088 linkTrackV.addLeftTool( sacSelectionV ); 089 spool.add( new Hold() 090 { 091 public void release() { sacSelectionV.removeFromParent(); } 092 }); 093 } 094 } 095 096 // Votespace SVG. 097 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 098 final Element yy = new HTML().getElement(); 099 y.appendChild( yy ); 100 yy.addClassName( "insulator" ); 101 yy.appendChild( svg.getElement() ); 102 103 // Controllers. 104 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 105 pathExtender = new PathExtender(); 106 new Modeller(); 107 } 108 109 110 111 // ```````````````````````````````````````````````````````````````````````````````````` 112 // init for early use 113 114 115 private final OMSVGDocument document = OMSVGParser.createDocument(); 116 117 118 private final OMSVGSVGElement svg = document.createSVGSVGElement(); 119 120 121 122 // ------------------------------------------------------------------------------------ 123 124 125 /** Appends the specified length to the length list. This is a convenience method. 126 */ 127 void append( final OMSVGAnimatedLengthList lengthList, final float length ) 128 { 129 lengthList.getBaseVal().appendItem( svg.createSVGLength( SVG_LENGTHTYPE_PX, 130 length * pxPerEm )); 131 } 132 133 134 135 /** The document of which the underlying SVG view is a component. 136 * 137 * @see #svgView() 138 */ 139 OMSVGDocument document() { return document; } 140 141 142 143 /** The votespace model on which this view is based. 144 */ 145 Votespace model() { return model; } 146 147 148 private final Votespace model; 149 150 151 152 /** Divisor for the threshold of votes below which a node is considered a "mosquito". 153 * This applies to a voter's outflow (carry + cast) volume compared to the 154 * candidate's receive volume, or to a base candidate's receive volume compared to 155 * total turnout. 156 */ 157 static final byte MOSQUITO_BAR_DIVISOR = 100; 158 159 160 161 /** The common path extender for use in node views. 162 */ 163 PathExtender pathExtender() { return pathExtender; } 164 165 166 private final PathExtender pathExtender; 167 168 169 170 /** The constant length of the em unit for the view's font, in pixels. Use this 171 * constant as a pixel multiplier instead of SVG_LENGTHTYPE_EMS for general layout 172 * purposes. This is a workaround for the surprising fact that em units for 173 * positioning in a container scale with the font size of the positioned object, not 174 * the container. This makes it hard to consistently position objects. 175 */ 176 float pxPerEm() { return pxPerEm; } 177 178 179 private float pxPerEm; // final after load 180 181 182 183 /** The spool for the release of associated holds. When unwound it releases the holds 184 * of this view, thereby disabling it. 185 */ 186 Spool spool() { return spool; } 187 188 189 private final Spool spool = new Spool1(); 190 191 192 193 /** Relative y offset of sub-mnemonic text (vote volume) from the associated mnemonic 194 * text. 195 */ 196 static final float SUB_MNEMONIC_DROP = 1.1f; // down below mnemonic 197 198 199 200 /** The 'svg' element of which the underlying SVG view is a descendant. 201 * 202 * @see #svgView() 203 */ 204 OMSVGSVGElement svg() { return svg; } 205 206 207 208 // - S V G - W r a p p e r ---------------------------------------------------------- 209 210 211 public final SVGNest<?> parent() { return null; } 212 213 214 215 public final OMSVGGElement svgView() { return svgView; } 216 217 218 private final OMSVGGElement svgView = document.createSVGGElement(); 219 220 { svg.appendChild( svgView ); } 221 222 223 224 // - W i d g e t ---------------------------------------------------------------------- 225 226 227 protected @Override void onLoad() 228 { 229 if( !spool.isUnwinding() ) { new Loader2(); } 230 super.onLoad(); 231 } 232 233 234 235 protected @Override void onUnload() 236 { 237 super.onUnload(); 238 spool.unwind(); 239 } 240 241 242 243 // ==================================================================================== 244 245 246 /** A controller that responds to node clicks by extending or contracting the votepath. 247 */ 248 final class PathExtender implements ClickHandler, PropertyChangeHandler 249 { 250 251 PathExtender() 252 { 253 spool.add( new Hold() 254 { 255 final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( 256 PropertyChange.TYPE, /*source*/model, PathExtender.this ); 257 public void release() { hR.removeHandler(); } 258 }); 259 } 260 261 262 /** Answers whether a dispatch of path extension events is currently in progress. 263 * This flag is raised when a path extension request is initiated by a node 264 * click, and lowered after the related events have cleared the votespace model. 265 */ 266 boolean isPathExtending() { return isPathExtending; } 267 268 private boolean isPathExtending; 269 270 271 public void onClick( final ClickEvent e ) 272 { 273 try 274 { 275 final Element sourceElement = ((OMSVGElement)e.getSource()).getElement(); 276 String votepathV = sourceElement.getPropertyString( "votepath" ); 277 if( votepathV == null ) return; // probably a node that is supposed to be un-displayed, and was displayed for test purposes 278 279 final String votepath = model.votepath(); 280 Element prunedElement = null; 281 if( votepath.endsWith( votepathV )) // clicked inside of current votepath 282 { 283 prunedElement = sourceElement; 284 votepathV = votepathV.substring( 1, votepathV.length() ); // prune back 285 } 286 else 287 { 288 if( votepathV.length() == 1 // clicked on an end-candidate 289 && votepath.length() > 0 ) // when another end-candidate's branch was expanded 290 { 291 final VoterCircle expandedCircle = centerCircle.outCircle(); 292 if( expandedCircle.isVisible() ) // true, unless there was some problem 293 { 294 prunedElement = expandedCircle.candidateV().localView().getElement(); 295 } 296 } 297 final String last = sourceElement.getPropertyString( "votepathLast" ); 298 if( last != null && last.endsWith( votepathV )) votepathV = last; // re-expand 299 } 300 if( prunedElement != null ) 301 { 302 prunedElement.setPropertyString( "votepathLast", model.votepath() ); 303 } 304 305 isPathExtending = true; 306 Scenes.i().sScopingSwitch().set( DartScoping.appendSwitch( GWTX.stringBuilderClear(), 307 model.pollName(), votepathV ).toString() ); 308 } 309 catch( Exception x ) { GWTX.handleUncaughtException( x ); } // q.v. for reason 310 } 311 312 313 public void onPropertyChange( final PropertyChange e ) 314 { 315 final String n = e.propertyName(); 316 if( "pollName".equals(n) || "votepath".equals(n)) 317 { 318 Scheduler.get().scheduleFinally( pathExtensionTerminator ); 319 } 320 } 321 322 323 private Scheduler.ScheduledCommand pathExtensionTerminator = new Scheduler.ScheduledCommand() 324 { 325 public void execute() { isPathExtending = false; } 326 }; 327 328 329 }; 330 331 332 333//// P r i v a t e /////////////////////////////////////////////////////////////////////// 334 335 336 /** The view of the total volume of votes cast, or poll turnout. It sits just under 337 * the poll name at midpoint of the center circle. 338 */ 339 private final FlowVolumeV castVolumeV = new FlowVolumeV( VotespaceV.this ); 340 341 { 342 castVolumeV.addClassNameBaseVal( "castVolumeV" ); 343 } 344 345 346 347 private CenterCircle centerCircle; // final after load 348 349 350 351 /** Vertical offset of mnemonic text from its focal point. 352 */ 353 private static final float POLLNAME_OFFSET_Y = -0.4f; // up 354 355 356 357 private final OMText pollNameTextNode = document.createTextNode( "" ); 358 359 360 361 // ==================================================================================== 362 363 364 @Warning("dead code") 365 private final class Loader implements com.google.gwt.event.dom.client.LoadHandler 366 { 367 368 Loader() 369 { 370 spool.add( new Hold() 371 { 372 final HandlerRegistration hR = svg.addLoadHandler( Loader.this ); 373 public void release() { hR.removeHandler(); } 374 }); 375 // SVGUnload not currently supported, so use VotespaceV.onUnload() instead. 376 // http://www.vectomatic.org/lib-gwt-svg/svg-event-mapping 377 } 378 379 380 public void onLoad( final com.google.gwt.event.dom.client.LoadEvent e ) 381 { 382 // never called though SVG clearly displayed, using VotespaceV.onLoad() instead. 383 com.google.gwt.user.client.Window.alert( "loading" ); 384 } 385 386 } 387 388 389 390 // ==================================================================================== 391 392 393 private final class Loader2 implements Scheduler.ScheduledCommand 394 { 395 396 Loader2() 397 { 398 standardText = document.createSVGTextElement(); 399 svg.appendChild( standardText ); 400 final short em = org.vectomatic.dom.svg.OMSVGLength.SVG_LENGTHTYPE_EMS; 401 standardText.getX().getBaseVal().appendItem( svg.createSVGLength( em, 1f )); 402 standardText.getY().getBaseVal().appendItem( svg.createSVGLength( em, 1f )); 403 standardText.appendChild( document.createTextNode( "standardText" )); 404 405 Scheduler.get().scheduleDeferred( Loader2.this ); 406 } 407 408 409 public void execute() // after Chrome is ready to support emLength.getValue() 410 { 411 // Measure em length from standard text. 412 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 413 try 414 { 415 final OMSVGLength emLen = standardText.getX().getBaseVal().getItem( 0 ); 416 pxPerEm = emLen.getValue(); 417 } 418 finally{ svg.removeChild( standardText ); } 419 420 // Center per a/web/context/xf/VotespaceV.css. 421 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 422 { 423 final float xActualPx = 3000; // formal (0,0) to actual (3000,2000) 424 final float yActualPx = 2000; 425 svgView.setAttribute( "transform", "translate(" // per TRANS_ATT 426 + xActualPx + " " + yActualPx + ")" ); 427 } 428 429 // Finish constructing the view. 430 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 431 float y = 0f; 432 OMSVGTextElement text; 433 434 // poll name 435 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 436 text = document.createSVGTextElement(); 437 svgView.appendChild( text ); 438 text.addClassNameBaseVal( "pollName" ); 439 text.getY().getBaseVal().appendItem( svg.createSVGLength( 440 SVG_LENGTHTYPE_PX, (y += POLLNAME_OFFSET_Y) * pxPerEm )); 441 text.appendChild( pollNameTextNode ); 442 443 // cast volume 444 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 445 svgView.appendChild( castVolumeV ); 446 castVolumeV.getY().getBaseVal().appendItem( svg.createSVGLength( 447 SVG_LENGTHTYPE_PX, (y += SUB_MNEMONIC_DROP) * pxPerEm )); 448 449 // center circle 450 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 451 centerCircle = new CenterCircle(VotespaceV.this); 452 centerCircle.setParent( VotespaceV.this ); 453 } 454 455 456 private final OMSVGTextElement standardText; 457 } 458 459 460 461 // ==================================================================================== 462 463 464 private final class Modeller implements PropertyChangeHandler, SelectionChangeEvent.Handler 465 { 466 467 Modeller() 468 { 469 spool.add( new Hold() 470 { 471 final HandlerRegistration hR = model.addSelectionChangeHandler( 472 Modeller.this ); 473 public void release() { hR.removeHandler(); } 474 }); 475 spool.add( new Hold() 476 { 477 final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( 478 PropertyChange.TYPE, /*source*/model, Modeller.this ); 479 public void release() { hR.removeHandler(); } 480 }); 481 remodelPollName(); // init state 482 remodelSac(); 483 } 484 485 486 private final @Warning("init call") void remodelPollName() 487 { 488 final String pollName = model().pollName(); 489 if( pollName == null ) 490 { 491 pollNameTextNode.setData( 492 App.i().mesS().gwt_scene_vote_VotespaceV_pollNameUnspecified() ); 493 } 494 else pollNameTextNode.setData( pollName ); 495 } 496 497 498 private final @Warning("init call") void remodelSac() 499 { 500 final SacJS sac = model.getSac(); 501 String newBodyClass = "countingMethods-"; 502 if( sac == null ) 503 { 504 assert model().count() == null; 505 castVolumeV.setPlaceholder( 506 App.i().mesS().gwt_scene_vote_VotespaceV_countUnknown() ); 507 } 508 else 509 { 510 final CountingMethodJS.SwitchMnemonic mCM = sac.countingMethodMnemonic(); 511 newBodyClass += mCM.name(); 512 if( mCM == CountingMethodJS.SwitchMnemonic.q ) 513 { 514 final SacJS_q qSac = sac.cast(); 515 castVolumeV.set( qSac.castVolume() ); 516 } 517 else if( mCM == CountingMethodJS.SwitchMnemonic.v ) 518 { 519 final SacJS_v vSac = sac.cast(); 520 castVolumeV.set( vSac.castVolume() ); 521 } 522 } 523 setBodyClass( newBodyClass ); 524 } 525 526 527 private final @Warning("init call") void setBodyClass( final String newBodyClass ) 528 { 529 if( newBodyClass.equals( bodyClass )) return; 530 531 final Element body = RootPanel.getBodyElement(); 532 if( bodyClass != null ) body.removeClassName( bodyClass ); // null on init only 533 534 bodyClass = newBodyClass; 535 body.addClassName( bodyClass ); 536 } 537 538 private String bodyClass; 539 540 541 // - P r o p e r t y - C h a n g e - H a n d l e r -------------------------------- 542 543 544 public void onPropertyChange( final PropertyChange e ) 545 { 546 if( "pollName".equals( e.propertyName() )) remodelPollName(); 547 } 548 549 550 // - S e l e c t i o n - C h a n g e - E v e n t . H a n d l e r ------------------ 551 552 553 public void onSelectionChange( SelectionChangeEvent _e ) { remodelSac(); } 554 555 } 556 557 558 559}