package 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. import com.google.gwt.core.client.*; import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.*; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.view.client.*; import com.google.web.bindery.event.shared.HandlerRegistration; import org.vectomatic.dom.svg.*; import org.vectomatic.dom.svg.utils.*; import votorola.a.count.gwt.*; import votorola.a.web.gwt.*; import votorola.g.hold.*; import votorola.g.lang.*; import votorola.g.web.gwt.*; import votorola.g.web.gwt.event.*; import votorola.g.web.gwt.svg.*; import votorola.s.gwt.scene.*; import votorola.s.gwt.stage.*; import votorola.s.gwt.stage.link.*; import static org.vectomatic.dom.svg.OMSVGLength.SVG_LENGTHTYPE_PX; /** A view of a {@linkplain Votespace votespace} in which the main component is * implemented as a scalable vector graphic. The overall layout of the page is as * follows:
  *
  *    +--------------------------------------------------------+
  *    |                        stage                           |
  *    +-------------+------------------------------------------+
  *    |             |                                          |
  *    |             |                                          |
  *    |             |                                          |
  *    |             |                                          |
  *    |             |                                          |
  *    |    feed     |                                          |
  *    |             |                  svg                     |
  *    |             |                                          |
  *    |             |                                          |
  *    |             |                                          |
  *    |             |                                          |
  *    |             |                                          |
  *    +-------------+------------------------------------------+
* *

Except where noted otherwise in the API, lengths for the vector graphic (svg) are * given in "em" units. Angles are given in degrees measured clockwise from the positive * (right hand) x axis.

* *

Acknowledgement: The design of this view follows from the suggestions of Thomas von * der Elbe, in a Skype discussion on April 11, 2011. See the Metagovernment IRC log and * the follow up post to the Metagovernment mailing list.

* * @see votorola.s.gwt.stage.StageIn * @see Live * example of a VotespaceV (right) */ @Warning("permanently disabled on detach from document") public final class VotespaceV extends HTML implements SVGNest> { // Note: Intermediate calculations are done in floats only because the SVG library // dislikes doubles. /** Constructs a VotespaceV. */ public VotespaceV( Votespace _model ) { model = _model; final Element y = getElement(); y.addClassName( "vote-VotespaceV" ); // Resource accounting view. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( Scenes.toUseRAC() ) { final LinkTrackV linkTrackV = LinkTrackV.i( StageV.i() ); if( linkTrackV != null ) { final SacSelectionV sacSelectionV = new SacSelectionV( model ); linkTrackV.addLeftTool( sacSelectionV ); spool.add( new Hold() { public void release() { sacSelectionV.removeFromParent(); } }); } } // Votespace SVG. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final Element yy = new HTML().getElement(); y.appendChild( yy ); yy.addClassName( "insulator" ); yy.appendChild( svg.getElement() ); // Controllers. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - pathExtender = new PathExtender(); new Modeller(); } // ```````````````````````````````````````````````````````````````````````````````````` // init for early use private final OMSVGDocument document = OMSVGParser.createDocument(); private final OMSVGSVGElement svg = document.createSVGSVGElement(); // ------------------------------------------------------------------------------------ /** Appends the specified length to the length list. This is a convenience method. */ void append( final OMSVGAnimatedLengthList lengthList, final float length ) { lengthList.getBaseVal().appendItem( svg.createSVGLength( SVG_LENGTHTYPE_PX, length * pxPerEm )); } /** The document of which the underlying SVG view is a component. * * @see #svgView() */ OMSVGDocument document() { return document; } /** The votespace model on which this view is based. */ Votespace model() { return model; } private final Votespace model; /** Divisor for the threshold of votes below which a node is considered a "mosquito". * This applies to a voter's outflow (carry + cast) volume compared to the * candidate's receive volume, or to a base candidate's receive volume compared to * total turnout. */ static final byte MOSQUITO_BAR_DIVISOR = 100; /** The common path extender for use in node views. */ PathExtender pathExtender() { return pathExtender; } private final PathExtender pathExtender; /** The constant length of the em unit for the view's font, in pixels. Use this * constant as a pixel multiplier instead of SVG_LENGTHTYPE_EMS for general layout * purposes. This is a workaround for the surprising fact that em units for * positioning in a container scale with the font size of the positioned object, not * the container. This makes it hard to consistently position objects. */ float pxPerEm() { return pxPerEm; } private float pxPerEm; // final after load /** The spool for the release of associated holds. When unwound it releases the holds * of this view, thereby disabling it. */ Spool spool() { return spool; } private final Spool spool = new Spool1(); /** Relative y offset of sub-mnemonic text (vote volume) from the associated mnemonic * text. */ static final float SUB_MNEMONIC_DROP = 1.1f; // down below mnemonic /** The 'svg' element of which the underlying SVG view is a descendant. * * @see #svgView() */ OMSVGSVGElement svg() { return svg; } // - S V G - W r a p p e r ---------------------------------------------------------- public final SVGNest parent() { return null; } public final OMSVGGElement svgView() { return svgView; } private final OMSVGGElement svgView = document.createSVGGElement(); { svg.appendChild( svgView ); } // - W i d g e t ---------------------------------------------------------------------- protected @Override void onLoad() { if( !spool.isUnwinding() ) { new Loader2(); } super.onLoad(); } protected @Override void onUnload() { super.onUnload(); spool.unwind(); } // ==================================================================================== /** A controller that responds to node clicks by extending or contracting the votepath. */ final class PathExtender implements ClickHandler, PropertyChangeHandler { PathExtender() { spool.add( new Hold() { final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( PropertyChange.TYPE, /*source*/model, PathExtender.this ); public void release() { hR.removeHandler(); } }); } /** Answers whether a dispatch of path extension events is currently in progress. * This flag is raised when a path extension request is initiated by a node * click, and lowered after the related events have cleared the votespace model. */ boolean isPathExtending() { return isPathExtending; } private boolean isPathExtending; public void onClick( final ClickEvent e ) { try { final Element sourceElement = ((OMSVGElement)e.getSource()).getElement(); String votepathV = sourceElement.getPropertyString( "votepath" ); if( votepathV == null ) return; // probably a node that is supposed to be un-displayed, and was displayed for test purposes final String votepath = model.votepath(); Element prunedElement = null; if( votepath.endsWith( votepathV )) // clicked inside of current votepath { prunedElement = sourceElement; votepathV = votepathV.substring( 1, votepathV.length() ); // prune back } else { if( votepathV.length() == 1 // clicked on an end-candidate && votepath.length() > 0 ) // when another end-candidate's branch was expanded { final VoterCircle expandedCircle = centerCircle.outCircle(); if( expandedCircle.isVisible() ) // true, unless there was some problem { prunedElement = expandedCircle.candidateV().localView().getElement(); } } final String last = sourceElement.getPropertyString( "votepathLast" ); if( last != null && last.endsWith( votepathV )) votepathV = last; // re-expand } if( prunedElement != null ) { prunedElement.setPropertyString( "votepathLast", model.votepath() ); } isPathExtending = true; Scenes.i().sScopingSwitch().set( DartScoping.appendSwitch( GWTX.stringBuilderClear(), model.pollName(), votepathV ).toString() ); } catch( Exception x ) { GWTX.handleUncaughtException( x ); } // q.v. for reason } public void onPropertyChange( final PropertyChange e ) { final String n = e.propertyName(); if( "pollName".equals(n) || "votepath".equals(n)) { Scheduler.get().scheduleFinally( pathExtensionTerminator ); } } private Scheduler.ScheduledCommand pathExtensionTerminator = new Scheduler.ScheduledCommand() { public void execute() { isPathExtending = false; } }; }; //// P r i v a t e /////////////////////////////////////////////////////////////////////// /** The view of the total volume of votes cast, or poll turnout. It sits just under * the poll name at midpoint of the center circle. */ private final FlowVolumeV castVolumeV = new FlowVolumeV( VotespaceV.this ); { castVolumeV.addClassNameBaseVal( "castVolumeV" ); } private CenterCircle centerCircle; // final after load /** Vertical offset of mnemonic text from its focal point. */ private static final float POLLNAME_OFFSET_Y = -0.4f; // up private final OMText pollNameTextNode = document.createTextNode( "" ); // ==================================================================================== @Warning("dead code") private final class Loader implements com.google.gwt.event.dom.client.LoadHandler { Loader() { spool.add( new Hold() { final HandlerRegistration hR = svg.addLoadHandler( Loader.this ); public void release() { hR.removeHandler(); } }); // SVGUnload not currently supported, so use VotespaceV.onUnload() instead. // http://www.vectomatic.org/lib-gwt-svg/svg-event-mapping } public void onLoad( final com.google.gwt.event.dom.client.LoadEvent e ) { // never called though SVG clearly displayed, using VotespaceV.onLoad() instead. com.google.gwt.user.client.Window.alert( "loading" ); } } // ==================================================================================== private final class Loader2 implements Scheduler.ScheduledCommand { Loader2() { standardText = document.createSVGTextElement(); svg.appendChild( standardText ); final short em = org.vectomatic.dom.svg.OMSVGLength.SVG_LENGTHTYPE_EMS; standardText.getX().getBaseVal().appendItem( svg.createSVGLength( em, 1f )); standardText.getY().getBaseVal().appendItem( svg.createSVGLength( em, 1f )); standardText.appendChild( document.createTextNode( "standardText" )); Scheduler.get().scheduleDeferred( Loader2.this ); } public void execute() // after Chrome is ready to support emLength.getValue() { // Measure em length from standard text. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - try { final OMSVGLength emLen = standardText.getX().getBaseVal().getItem( 0 ); pxPerEm = emLen.getValue(); } finally{ svg.removeChild( standardText ); } // Center per a/web/context/xf/VotespaceV.css. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final float xActualPx = 3000; // formal (0,0) to actual (3000,2000) final float yActualPx = 2000; svgView.setAttribute( "transform", "translate(" // per TRANS_ATT + xActualPx + " " + yActualPx + ")" ); } // Finish constructing the view. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - float y = 0f; OMSVGTextElement text; // poll name // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` text = document.createSVGTextElement(); svgView.appendChild( text ); text.addClassNameBaseVal( "pollName" ); text.getY().getBaseVal().appendItem( svg.createSVGLength( SVG_LENGTHTYPE_PX, (y += POLLNAME_OFFSET_Y) * pxPerEm )); text.appendChild( pollNameTextNode ); // cast volume // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` svgView.appendChild( castVolumeV ); castVolumeV.getY().getBaseVal().appendItem( svg.createSVGLength( SVG_LENGTHTYPE_PX, (y += SUB_MNEMONIC_DROP) * pxPerEm )); // center circle // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` centerCircle = new CenterCircle(VotespaceV.this); centerCircle.setParent( VotespaceV.this ); } private final OMSVGTextElement standardText; } // ==================================================================================== private final class Modeller implements PropertyChangeHandler, SelectionChangeEvent.Handler { Modeller() { spool.add( new Hold() { final HandlerRegistration hR = model.addSelectionChangeHandler( Modeller.this ); public void release() { hR.removeHandler(); } }); spool.add( new Hold() { final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( PropertyChange.TYPE, /*source*/model, Modeller.this ); public void release() { hR.removeHandler(); } }); remodelPollName(); // init state remodelSac(); } private final @Warning("init call") void remodelPollName() { final String pollName = model().pollName(); if( pollName == null ) { pollNameTextNode.setData( App.i().mesS().gwt_scene_vote_VotespaceV_pollNameUnspecified() ); } else pollNameTextNode.setData( pollName ); } private final @Warning("init call") void remodelSac() { final SacJS sac = model.getSac(); String newBodyClass = "countingMethods-"; if( sac == null ) { assert model().count() == null; castVolumeV.setPlaceholder( App.i().mesS().gwt_scene_vote_VotespaceV_countUnknown() ); } else { final CountingMethodJS.SwitchMnemonic mCM = sac.countingMethodMnemonic(); newBodyClass += mCM.name(); if( mCM == CountingMethodJS.SwitchMnemonic.q ) { final SacJS_q qSac = sac.cast(); castVolumeV.set( qSac.castVolume() ); } else if( mCM == CountingMethodJS.SwitchMnemonic.v ) { final SacJS_v vSac = sac.cast(); castVolumeV.set( vSac.castVolume() ); } } setBodyClass( newBodyClass ); } private final @Warning("init call") void setBodyClass( final String newBodyClass ) { if( newBodyClass.equals( bodyClass )) return; final Element body = RootPanel.getBodyElement(); if( bodyClass != null ) body.removeClassName( bodyClass ); // null on init only bodyClass = newBodyClass; body.addClassName( bodyClass ); } private String bodyClass; // - P r o p e r t y - C h a n g e - H a n d l e r -------------------------------- public void onPropertyChange( final PropertyChange e ) { if( "pollName".equals( e.propertyName() )) remodelPollName(); } // - 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 ------------------ public void onSelectionChange( SelectionChangeEvent _e ) { remodelSac(); } } }