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}