package 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. import com.google.gwt.core.client.Scheduler; import com.google.gwt.dom.client.*; import com.google.gwt.user.client.ui.*; import com.google.web.bindery.event.shared.HandlerRegistration; import java.util.*; import org.vectomatic.dom.svg.*; import org.vectomatic.dom.svg.utils.*; import votorola.a.count.*; import votorola.a.count.gwt.*; import votorola.g.hold.*; import votorola.g.lang.*; import votorola.g.web.gwt.*; import votorola.g.web.gwt.event.*; import votorola.s.gwt.stage.*; import static votorola.a.count.CountNode.DART_SECTOR_MAX; /** A view of a board rendered as an HTML table cell containing an SVG drawing. The * drawing is visible only when the board is enabled. It comprises a single {@linkplain * NodeV node view} for the {@linkplain Board#mosquito() mosquito} (0) followed by * {@value votorola.a.count.CountNode#DART_SECTOR_MAX} node views for the {@linkplain * Board#node(int) dart sectored nodes}. The length of each node view is roughly * proportional to its vote flow. Unoccupied nodes are indicated by subdued * styling.
  *
  *   +---+---+--------+---+- -    -+---+----------------+---+---+---+
  *    \   \   \        \   \        \   \                \   \   \   \
  *     +   +   +        +   +        +   +                +   +   +   +
  *    /   /   /        /   /        /   /                /   /   /   /
  *   +---+---+--------+---+- -    -+---+----------------+---+---+---+
  *     0   1     2      3  . . .     16                   18  19  20
*/ public class BoardV extends NodeV.Box { /** Partially constructs a BoardV for {@linkplain #init() init} to finish. * * @param _board the board on which the view is modelled. * @param element the outermost HTML element of which the view is composed. */ BoardV( Board _board, final VoteTrackV trackV, final TableCellElement element, XCastRelation _place ) { super( _place, trackV ); board = _board; // HTML view. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setElement( element ); // SVG view. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final OMSVGDocument svgDoc = OMSVGParser.createDocument(); svg = svgDoc.createSVGSVGElement(); element.appendChild( svg.getElement() ); nodes = svgDoc.createSVGGElement(); svg.appendChild( nodes ); final VoteTrack track = trackV.track(); for( int s = 0; s < MAX_NODE_COUNT; ++s ) { nodes.appendChild( new NodeV( BoardV.this, s, track )); } // Controllers. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - new Modeller( trackV ); } /** Completes the construction of this BoardV. Call once only. */ void init() { init( new Painter() ); } /** Completes the construction of this BoardV using a particular painter. Call once * only. * * @see #painter() */ void init( Painter _painter ) { painter = _painter; } // - C o u n t - N o d e . B o x ------------------------------------------------------ public final Iterable nodeViews() { return nodeViews; } private final Iterable nodeViews = new Iterable() { public Iterator iterator() { return new votorola.g.util.IteratorA() { private NodeV nextV = (NodeV)nodes.getFirstChild(); public boolean hasNext() { return nextV != null; } public NodeV next() { if( nextV == null ) throw new NoSuchElementException(); final NodeV nodeV = nextV; nextV = nodeV.getNextSibling(); return nodeV; } }; } }; /** The painter for this board. */ public final Painter painter() { return painter; } private Painter painter; // final after init /** The SVG component of this view. */ final OMSVGSVGElement svg() { return svg; } private final OMSVGSVGElement svg; // could not instead bind as "@UiField OMSVGSVGElement" because Element "is not // assignable to" OMSVGSVGElement // - W i d g e t ---------------------------------------------------------------------- protected @Override void onLoad() { if( !spool().isUnwinding() ) painter.init(); // after load, when size known [but unclear how init depends on size] super.onLoad(); } // ==================================================================================== /** A painter for a board. */ class Painter extends NodeVPainter { Painter() { super( BoardV.this ); } private void init() { init( spool() ); spool().add( new Hold() { final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( PropertyChange.TYPE, /*source*/Stage.i(), Painter.this ); public void release() { hR.removeHandler(); } }); repaint(); } // -------------------------------------------------------------------------------- /** Repaints the view. */ final @Override void repaint( final int width, final float protrusion, final int y, final float halfThickness ) { if( !board.isEnabled() ) return; // await call from remodel palette.recalibrate( width, protrusion, board.isEndBoard() ); // int s = 0; // dart sector, starts at mosquito (0) // final int sAnchor; // { // final CountNodeJS anchor = board.track().anchor(); // sAnchor = anchor == null? -1: anchor.dartSector(); // } /// anchor node null till voters fetched, so use anchor *name*: final String anchorName = VoteTrack.anchorName( Stage.i() ); float x = 0; float xAnchor = -1; // unless found float lengthAnchor = -1; for( NodeV child = (NodeV)nodes.getFirstChild(); child != null; child = child.getNextSibling() ) { final CountNodeJS node = child.getCountNode(); final float length = palette.calculateLength( node ); child.repaint( x, length, protrusion, y, halfThickness ); // if( s == sAnchor ) if( node != null && node.name().equals( anchorName )) { xAnchor = x; lengthAnchor = length; } // ++s; x += length; x += MARGIN_INTER; } repaint2( protrusion, y, halfThickness, xAnchor, lengthAnchor ); GWTX.i().bus().fireEventFromSource( new Change(), Painter.this ); } /** Repaints extended parts of the view. The implementation of this method in the * base class Painter does nothing. */ void repaint2( float _protrusion, int _y, float _halfThickness, float _xAnchor, float _lengthAnchor ) {} // - P r o p e r t y - C h a n g e - H a n d l e r -------------------------------- public final @Override void onPropertyChange( final PropertyChange e ) { if( spool().isUnwinding() ) return; super.onPropertyChange( e ); if( e.getSource() == Stage.i() && e.propertyName().equals("differencesShadowed") ) { repaint(); } } } // ==================================================================================== /** A workspace for intermediate calculations in painting a board. */ static final class Palette { private float apportionableWidth; private LengthApportioner apportioner; /** Calculates the length of the node view for the specified node. */ float calculateLength( final CountNodeJS node ) { final float portion = apportioner.calculatePortion( node ); return minLength + portion * apportionableWidth; } private final LengthApportioner outflowApportioner = new LengthApportioner() { float calculatePortion( final CountNodeJS node ) { final float portion; if( node == null ) portion = 0; else { final SacRegisterJS_v reg = node.voteRegister(); portion = ((float)reg.carryVolume() + (float)reg.castVolume()) / outflowTotal; } return portion; } }; /** The total of cast and carried volume for all nodes in the board. */ long outflowTotal; private float minLength; private final LengthApportioner occupantApportioner = new LengthApportioner() { float calculatePortion( final CountNodeJS node ) { final int weightTotal = MAX_NODE_COUNT + occupantCount; final int weight; if( node == null ) weight = 1; else weight = 2; return (float)weight / weightTotal; } }; /** The count of non-null nodes in the board. */ int occupantCount; /** Recalibrates to new board dimensions. * * @param width the rendered width of the {@linkplain * NodeVPainter#container HTML container} of the node views. */ void recalibrate( final int width, final float protrusion, final boolean isEndBoard ) { final float lengthSum = width - protrusion - (MAX_NODE_COUNT-1) * MARGIN_INTER; // total of width actually occupied by nodes apportionableWidth = lengthSum - MAX_NODE_COUNT * PREFERRED_MIN_LENGTH; // amount of lengthSum to be apportioned based on received volume, such that // nodes with greater volume are rendered in greater length if( apportionableWidth < 0 ) { minLength = lengthSum / MAX_NODE_COUNT; apportionableWidth = 0; } else minLength = PREFERRED_MIN_LENGTH; if( isEndBoard ) apportioner = receiveVolumeApportioner; else if( outflowTotal > 0 ) apportioner = outflowApportioner; else apportioner = occupantApportioner; // probably a lone mosquito on board } private final LengthApportioner receiveVolumeApportioner = new LengthApportioner() { float calculatePortion( final CountNodeJS node ) { final float portion; if( node == null ) portion = 0; else portion = (float)node.voteRegister().receiveVolume() / receiveTotal; return portion; } }; /** The total of received volume for all nodes in the board. */ long receiveTotal; } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private final Board board; private static final int MARGIN_INTER = NodeV.STROKE_WIDTH > 0? 0: 3; // angled as it is, the 2 pixel stroke alone is roughly equivalent to a 3 pixel margin private static final int MAX_NODE_COUNT = DART_SECTOR_MAX + 1; private final OMSVGGElement nodes; private final Palette palette = new Palette(); private static final int PREFERRED_MIN_LENGTH = 7/*apparent*/ + NodeV.STROKE_WIDTH; // ==================================================================================== private static abstract class LengthApportioner { abstract float calculatePortion( CountNodeJS node ); } // ==================================================================================== private final class Modeller extends SuspendedModeller { Modeller( final VoteTrackV trackV ) { super( trackV, spool() ); remodelUnlessMoving(); // init state } final @Warning("init call") void remodel() { if( board.isEnabled() ) { palette.occupantCount = 0; palette.outflowTotal = 0; palette.receiveTotal = 0; wasChanged = false; // for( final NodeV v: nodeViews() ) remodel( v, board.node(v.dartSector()) ); /// cannot fetch mosquito thus from board, so: NodeV v = (NodeV)nodes.getFirstChild(); remodel( v, board.mosquito() ); for( int s = 1; s <= DART_SECTOR_MAX; ++s ) { v = v.getNextSibling(); remodel( v, board.node(s) ); } if( palette.occupantCount > 0 ) { setVisible( true ); if( painter == null || !wasChanged ) return; // save expensive repaint Scheduler.get().scheduleFinally( new Scheduler.ScheduledCommand() { public void execute() { painter.repaint(); } // after browser's layout engine responds to changes above }); return; } // else probably a voters board in which there are no actual voters } setVisible( false ); } private final @Warning("init call") void remodel( final NodeV child, final CountNodeJS node ) { if( child.setCountNode( node )) wasChanged = true; if( node == null ) return; ++palette.occupantCount; final SacRegisterJS_v reg = node.voteRegister(); palette.outflowTotal += reg.carryVolume() + reg.castVolume(); palette.receiveTotal += reg.receiveVolume(); } private boolean wasChanged; } }