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.event.dom.client.ClickEvent;
004import com.google.gwt.event.dom.client.ClickHandler;
005import com.google.gwt.event.dom.client.MouseOutEvent;
006import com.google.gwt.event.dom.client.MouseOutHandler;
007import com.google.gwt.event.dom.client.MouseOverEvent;
008import com.google.gwt.event.dom.client.MouseOverHandler;
009import com.google.gwt.event.shared.GwtEvent;
010import com.google.web.bindery.event.shared.HandlerRegistration;
011import org.vectomatic.dom.svg.*;
012import votorola.a.count.*;
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.stage.*;
021import votorola.s.gwt.stage.light.*;
022
023import static com.google.gwt.dom.client.Style.Display.INLINE;
024import static votorola.a.count.XCastRelation.VOTER;
025import static votorola.a.count.XCastRelation.CO_VOTER;
026import static votorola.a.count.XCastRelation.CANDIDATE;
027
028
029/** A view of a count node in the shape of an arrow segment.  Clicking on the view sets or
030  * unsets the node as the {@linkplain Stage#getActorName() stage actor}.<pre>
031  *
032  *                       protrusion
033  *                         /
034  *       |---- length ---|--|
035  *
036  *    p0 +---------------+    -
037  *        \               \   | half thickness
038  *         +       *       +  -
039  *        /               /
040  *       +---------------+</pre>
041  *
042  * If the node is a <a href='../../../../../../../d/theory.xht#cycle'
043  * target='_top'>cycler</a>, then a spot (*) is visible in the middle of the view.
044  *
045  *     @see <a href='VoteTrackV.html#ackC'>Acknowledgement to Christian Weilbach</a>
046  */
047public class NodeV extends OMSVGGElement
048{
049
050
051    /** Constructs a NodeV.
052      *
053      *     @see #box()
054      *     @see #dartSector()
055      */
056    NodeV( Box _box, int _dartSector, final VoteTrack track )
057    {
058        // track needed only because box.trackV is null during init
059        box = _box;
060        dartSector = _dartSector;
061
062      // View.
063      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
064        addClassNameBaseVal( "CountNode" );
065        final XCastRelation place = box.place();
066        String s = Character.toString( place.symbol() );
067        if( place == XCastRelation.VOTER || place == XCastRelation.CO_VOTER ) s += dartSector;
068        dartLightClassName = s;
069        addClassNameBaseVal( dartLightClassName );
070        appendChild( new ArrowSegment() );
071        appendChild( new CycleSpot() );
072
073      // Controllers.
074      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
075        new Clicker();
076        highlighter = new Highlighter();
077        new Mouser();
078        sensor = new Sensor();
079        tracker = new Tracker( track );
080    }
081
082
083
084    /** Forces static initialization of this class and appends a post-configuration
085      * initializer to the specified spool.
086      *
087      *     @param configurationSpool a spool that will unwind immediately after
088      *       {@linkplain StageMod module configuration} is complete.
089      */
090    public static void forceInitClass( final Spool configurationSpool )
091    {
092        configurationSpool.add( new Hold()
093        {
094            public void release()
095            {
096                if( deselectionGuard == null ) deselectionGuard = new DefaultDeselect();
097            }
098        });
099    }
100
101
102
103   // ------------------------------------------------------------------------------------
104
105
106    /** The ancestral container of this view, the {@linkplain MajorV#spool() spool} of
107      * which controls its life cycle.
108      */
109    public final Box box() { return box; }
110
111
112        private final Box box;
113
114
115
116    /** The CSS class name for the {@linkplain DartLight dart light}, such as "v17".
117      */
118    final String dartLightClassName() { return dartLightClassName; }
119
120
121        private final String dartLightClassName;
122
123
124
125    /** The dart sector to which this view is restricted, or zero if it is not restricted
126      * to any particular sector.
127      *
128      *     @see votorola.a.count.CountNode#dartSector()
129      */
130    final int dartSector() { return dartSector; }
131
132
133        private final int dartSector;
134
135
136
137    /** The count node on which this view is modelled, or null if none is modelled.
138      *
139      *     @see #setCountNode(CountNodeJS)
140      */
141    public final CountNodeJS getCountNode() { return countNode; } // bearing in mind this is a subclass of OMNode
142
143
144        private CountNodeJS countNode;
145
146
147        /** Sets the count node on which this view is modelled.
148          *
149          *     @return true if setting the node resulted in a change, false if that node
150          *       was already set.
151          *     @see #getCountNode()
152          *     @throws IllegalArgumentException if the node does not have the same dart
153          *       sector as this view.
154          */
155        final boolean setCountNode( final CountNodeJS node )
156        {
157            if( ObjectX.nullEquals( node, countNode )) return false;
158              // Guard against pointless repaint, though it is not currently expensive.
159              // This is valid only because the node is immutable except its 'voters'
160              // array, which has no bearing on the view.
161
162            final VoteTrack track = box.trackV().track();
163            if( node == null )
164            {
165                if( countNode != null ) removeClassNameBaseVal( "occupied" ); // it changed
166                clearCycler();
167                sensor.clear();
168            }
169            else
170            {
171                if( dartSector != 0 && node.dartSector() != dartSector )
172                {
173                    throw new IllegalArgumentException( "dart sector mismatch" );
174                }
175
176                if( countNode == null ) addClassNameBaseVal( "occupied" ); // it changed
177                if( node.isCycler() ) setCycler();
178                else clearCycler();
179                sensor.set( track.count(), node );
180            }
181            countNode = node;
182            tracker.run( track );
183            highlighter.relight();
184            return true;
185        }
186
187
188
189    /** Answers whether the node is in a tight cycle with the anchor.
190      *
191      *     @see <a href='../../../../../../../d/theory.xht#cycle' target='_top'>tight cycle</a>
192      */
193    final boolean isTightCycler() { return isTightCycler; }
194
195
196        private boolean isTightCycler;
197
198
199
200    /** The horizontal length of the top line of the drawing as currently drawn in the SVG
201      * viewport.
202      */
203    public final float length()
204    {
205        final OMSVGPathSegLinetoRel seg = (OMSVGPathSegLinetoRel)
206          arrowSegment().getPathSegList().getItem( 1 );
207        return seg.getX();
208    }
209
210
211
212    /** The point at the top left of the drawing as currently drawn in the SVG viewport.
213      */
214    public final OMSVGPathSegMovetoAbs p0()
215    {
216        return (OMSVGPathSegMovetoAbs)arrowSegment().getPathSegList().getItem( 0 );
217    }
218
219
220
221    /** The horizontal extent of the arrowhead beyond the {@linkplain #length() length},
222      * as currently drawn in the SVG viewport.
223      */
224    public final float protrusion()
225    {
226        final OMSVGPathSegLinetoRel seg = (OMSVGPathSegLinetoRel)
227          arrowSegment().getPathSegList().getItem( 2 );
228        return seg.getX();
229    }
230
231
232
233    /** Redraws this view according to the provided parameters.  The view never
234      * automatically initiates a repaint, but depends entirely on its MajorV ancestor for
235      * this.
236      */
237    void repaint( final float x, float length, final float protrusion, final int y,
238      final float halfThickness )
239    {
240        arrowSegment().repaint( x, length, protrusion, y, halfThickness );
241        cycleSpot().repaint( x, length, protrusion, y, halfThickness );
242    }
243
244
245
246    /** Sets the type of {@linkplain DeselectionGuard deselection guard}.  Call this
247      * method from the global configuration function {@linkplain StageMod
248      * voGWTConfig.s_gwt_stage} in this fashion:<pre>
249      *
250      *   s_gwt_stage_vote_CountNodeV_setDeselectionGuard( 'Blind' );
251      *     // default is 'Default'</pre>
252      *
253      * Or call it at any time during normal operation.
254      *
255      *     @throws IllegalArgumentException if the provided guard name is unrecognized.
256      */
257    public static @GWTConfigCallback void setDeselectionGuard( final String guardName )
258    {
259        final DeselectionGuard g;
260        if( BlindDeselect.NAME.equals( guardName )) g = new BlindDeselect();
261        else if( DefaultDeselect.NAME.equals( guardName )) g = new DefaultDeselect();
262        else if( LaxDeselect.NAME.equals( guardName )) g = new LaxDeselect();
263        else throw new IllegalArgumentException( "no such guard: " + guardName );
264
265        deselectionGuard = g;
266        GWTX.i().bus().fireEventFromSource( new PropertyChange("deselectionGuard"),
267          NodeV.class );
268    }
269
270
271        /** The value is bound via the {@linkplain GWTX#bus() event bus} to property name
272          * <tt>deselectionGuard</tt> on source NodeV.class.
273          */
274        private static DeselectionGuard deselectionGuard;
275
276
277        private static native void exposeDeselectionGuard()
278        /*-{
279            $wnd.s_gwt_stage_vote_CountNodeV_setDeselectionGuard = $entry(
280              @votorola.s.gwt.stage.vote.NodeV::setDeselectionGuard(Ljava/lang/String;) );
281        }-*/;
282
283
284        static
285        {
286            assert StageMod.isForcedInit(): "forced init " + NodeV.class.getName();
287            exposeDeselectionGuard();
288        }
289
290
291
292    /** The stroke width for painting the main figure (arrow segment).
293      */
294    public static final int STROKE_WIDTH = 2; // from stage/vote/track.css
295
296
297
298   // - O  M - N o d e -------------------------------------------------------------------
299
300
301    public @Override final void fireEvent( final GwtEvent<?> e )
302    {
303        try{ super.fireEvent( e ); }
304        catch( Exception x ) { GWTX.handleUncaughtException( x ); } // q.v. for reason
305    }
306
307
308
309    public @Override final NodeV getNextSibling() { return (NodeV)super.getNextSibling(); }
310      // auto and explicit {@inheritDoc} fail, JDK 1.7
311
312
313
314    public @Override final NodeV getPreviousSibling() // ditto {@inheritDoc} failure
315    {
316        return (NodeV)super.getPreviousSibling();
317    }
318
319
320
321   // ====================================================================================
322
323
324 // /** The {@value NAME} deselection guard.  It allows for deselection of nodes that are
325 //   * base candidates or non-participants (both being tracked at the base), provided no
326 //   * {@linkplain Stage#getDefaultActorName() default actor} is set.  This allows the
327 //   * track to fall cleanly into its default state where only the base peer board is
328 //   * displayed.  This will not normally happen when a default actor is set, of course,
329 //   * and that is why deselection is disabled in that case.
330 //   */
331 // static final class BaseCandidateDeselect implements DeselectionGuard
332 // {
333 //
334 //     public void guard( final NodeV nodeV )
335 //     {
336 //         final CountNodeJS node = nodeV.countNode;
337 //         nodeV.isClickable = Stage.i().getDefaultActorName() == null && ( node.isBaseCandidate()
338 //          ||/*non-participant*/ !node.isVoter() && !node.isCandidate() );
339 //     }
340 //
341 //
342 //     static final String NAME = "BaseCandidate";
343 //
344 // }
345 ///// But the track is pinned when a default actor is set, so its unclear why deselection
346 ///// should be forbidden in that case.  Being pinned, the track won't snap back far.
347
348
349
350   // ====================================================================================
351
352
353 // /** The {@value NAME} deselection guard.  It is the same as {@linkplain
354 //   * BaseCandidateDeselect BaseCandidateDeselect} except it always allows for the
355 //   * deselection of mosquitos.  This is useful because there is ordinarily no way to
356 //   * navigate away from a mosquito once it is selected except by deselecting it.
357 //   */
358 // static final class BaseMosquitoDeselect implements DeselectionGuard
359 // {
360 //
361 //     public void guard( final NodeV nodeV )
362 //     {
363 //         final CountNodeJS node = nodeV.countNode;
364 //         nodeV.isClickable = node.dartSector() == 0
365 //           || Stage.i().getDefaultActorName() == null && node.isBaseCandidate();
366 //     }
367 //
368 //
369 //     static final String NAME = "BaseMosquito";
370 //
371 // }
372 ///// But a mosquito may be deselected by selecting another node, so the use case is
373 ///// unclear.  Maybe the intention was to deselect a non-participant node, because
374 ///// otherwise it may get stuck.
375
376
377
378   // ====================================================================================
379
380
381    /** The {@value NAME} deselection guard.  It allows for deselection of any node.  If a
382      * {@linkplain Stage#getDefaultActorName() default actor} is set, then it cannot
383      * actually be deselected.  But this guard will not prevent the attempt.
384      */
385    static final class BlindDeselect implements DeselectionGuard
386    {
387
388        public void guard( final NodeV nodeV ) { nodeV.isClickable = true; }
389
390
391        static final String NAME = "Blind";
392
393    }
394
395
396
397   // ====================================================================================
398
399
400    /** A container of node views.
401      */
402    public static abstract class Box extends MajorV
403    {
404
405        /** Creates a new Box.
406          *
407          *     @see MajorV#place()
408          *     @see MajorV#trackV()
409          */
410        Box( XCastRelation _place, VoteTrackV _trackV ) { super( _place, _trackV ); }
411
412
413       // ------------------------------------------------------------------------------------
414
415
416        /** The nodal components of this view.
417          */
418        public abstract Iterable<NodeV> nodeViews();
419
420
421        /** The source of {@linkplain Change change events} that are fired on the
422          * {@linkplain GWTX#bus() bus} after each repainting of the node views.
423          */
424        public abstract Object painter();
425
426    }
427
428
429
430   // ====================================================================================
431
432
433    /** The {@value NAME} deselection guard.  It disallows deselection of a {@linkplain
434      * Stage#getDefaultActorName() default actor} node, because such a node cannot really
435      * be deselected in any case.  If the track is {@linkplain VoteTrack#toPin(Stage)
436      * unpinned}, then it also disallows the deselection of all but base candidate and
437      * non-participant nodes.  Deselection will cause the unpinned track to drop to its
438      * default state in which only the base peer board is displayed.  The user may easily
439      * reselect a base candidate or non-participant in that state, while other nodes
440      * might require a climb back up the tree.
441      */
442    static final class DefaultDeselect implements DeselectionGuard
443    {
444
445        public void guard( final NodeV nodeV )
446        {
447            final Stage stage = Stage.i();
448            final CountNodeJS node = nodeV.countNode;
449            if( VoteTrack.toPin( stage ))
450            {
451                nodeV.isClickable = !node.name().equals( stage.getDefaultActorName() );
452            }
453            else // unpinned track
454            {
455                assert stage.getDefaultActorName() == null; // node cannot be default actor
456                nodeV.isClickable = node.isBaseCandidate()
457                 ||/*non-participant*/ !node.isVoter() && !node.isCandidate();
458            }
459        }
460
461
462        static final String NAME = "Default";
463
464    }
465
466
467
468   // ====================================================================================
469
470
471    /** A controller that determines whether a selected node view may be deselected.
472      */
473    interface DeselectionGuard
474    {
475
476        /** Sets nodeV.{@linkplain NodeV#isClickable isClickable} according to whether
477          * nodeV may be deselected.  nodeV must be modelled by an actual node.
478          */
479        public void guard( final NodeV nodeV );
480
481    }
482
483
484
485   // ====================================================================================
486
487
488    /** The {@value NAME} deselection guard.  It allows for deselection of any node except
489      * that of the {@linkplain Stage#getDefaultActorName() default actor}, which cannot
490      * actually be deselected in any case.
491      */
492    static final class LaxDeselect implements DeselectionGuard
493    {
494
495        public void guard( final NodeV nodeV )
496        {
497            nodeV.isClickable = !nodeV.countNode.name().equals( Stage.i().getDefaultActorName() );
498        }
499
500
501        static final String NAME = "Lax";
502
503    }
504
505
506
507   // ====================================================================================
508
509
510    /** A lighting sensor for a node view.
511      */
512    final class Sensor extends Sensor1 implements NodalSensor
513    {
514
515        private Sensor()
516        {
517            Stage.i().lightBank().addSensor( Sensor.this );
518            box.spool().add( new Hold()
519            {
520                public void release() { Stage.i().lightBank().removeSensor( Sensor.this ); }
521            });
522        }
523
524
525        private void clear()
526        {
527            if( pollName == null )
528            {
529                assert personName == null;
530                return; // already clear
531            }
532
533            displayTitle = null;
534            personName = null;
535            pollName = null;
536            changed();
537        }
538
539
540        private void set( final CountJS count, final CountNodeJS node )
541        {
542            if( count == null || node == null )
543            {
544                clear();
545                return;
546            }
547
548            displayTitle = node.displayTitle();
549            personName = node.name();
550            pollName = count.pollName();
551            changed();
552        }
553
554
555        /** The node view associated with this lighting sensor.
556          */
557        NodeV view() { return NodeV.this; }
558
559
560       // - N o d a l - S e n s o r ------------------------------------------------------
561
562
563        public NodeV.Box box() { return box; }
564
565
566        public String displayTitle() { return displayTitle; }
567
568
569            private String displayTitle;
570
571
572       // - P o s i t i o n - S e n s o r ------------------------------------------------
573
574
575        public String personName() { return personName; }
576
577
578            private String personName;
579
580
581        public String pollName() { return pollName; }
582
583
584            private String pollName;
585
586    }
587
588
589
590//// P r i v a t e ///////////////////////////////////////////////////////////////////////
591
592
593    private ArrowSegment arrowSegment() { return (ArrowSegment)getFirstChild(); }
594
595
596
597    private CycleSpot cycleSpot() { return (CycleSpot)getLastChild(); }
598
599
600
601    private final Highlighter highlighter;
602
603
604
605    private boolean isClickable;
606
607
608
609    /** Answers whether the node is a {@linkplain CountNode#isCycler cycler}, returning
610      * false if the node is null.
611      */
612    private boolean isCycler() { return countNode == null? false: countNode.isCycler(); }
613
614
615        private void setCycler() { cycleSpot().getStyle().setDisplay( INLINE ); }
616
617
618        private void clearCycler() { cycleSpot().getStyle().clearDisplay(); }
619          // default to "none" as set in vote/track.css
620
621
622
623    private final Sensor sensor;
624
625
626
627    private final Tracker tracker;
628
629
630
631   // ====================================================================================
632
633
634    private final class ArrowSegment extends OMSVGPathElement
635    {
636
637        ArrowSegment()
638        {
639            addClassNameBaseVal( "ArrowSegment" );
640            final OMSVGPathSegList sL = getPathSegList();
641       /*0*/sL.appendItem( createSVGPathSegMovetoAbs( 0,0 ));
642       /*1*/sL.appendItem( createSVGPathSegLinetoRel( 0,0 ));
643       /*2*/sL.appendItem( createSVGPathSegLinetoRel( 0,0 ));
644       /*3*/sL.appendItem( createSVGPathSegLinetoRel( 0,0 ));
645       /*4*/sL.appendItem( createSVGPathSegLinetoRel( 0,0 ));
646       /*5*/sL.appendItem( createSVGPathSegLinetoRel( 0,0 ));
647       /*0*/sL.appendItem( createSVGPathSegClosePath() );
648        }
649
650
651        void repaint( final float x, final float length, final float protrusion, final int y,
652          final float halfThickness )
653        {
654            final OMSVGPathSegList sL = getPathSegList();
655
656              //   0              1
657              //    +--------------+
658              //     \              \
659              //      + 5            + 2
660              //     /              /
661              //    +--------------+
662              //   4               3
663
664            PathSeg.movetoAbs( sL, 0,  x, y );
665            PathSeg.lineToRel( sL, 1,  length, 0 );
666            PathSeg.lineToRel( sL, 2,  protrusion,  halfThickness );
667            PathSeg.lineToRel( sL, 3, -protrusion,  halfThickness );
668            PathSeg.lineToRel( sL, 4, -length, 0 );
669            PathSeg.lineToRel( sL, 5,  protrusion, -halfThickness );
670        }
671
672    }
673
674
675
676   // ====================================================================================
677
678
679    private final class Clicker implements ClickHandler
680    {
681
682        Clicker() { addClickHandler( Clicker.this ); } // no need to unregister, registry does not outlive the handler
683
684
685        public void onClick( ClickEvent _e )
686        {
687            if( !isClickable ) return;
688
689            String name = countNode.name();
690            if( name.equals( Stage.i().getActorName() )) name = null; // toggle off
691            Stage.setActorName( name );
692        }
693
694    }
695
696
697
698   // ====================================================================================
699
700
701    private final class CycleSpot extends OMSVGCircleElement
702    {
703
704        CycleSpot() { addClassNameBaseVal( "CycleSpot" ); }
705
706
707        private static final float MAX_RADIUS = 2f;
708
709
710        void repaint( final float x, final float length, final float protrusion, final int y,
711          final float halfThickness )
712        {
713            if( !isCycler() )
714            {
715                assert !INLINE.getCssName().equals( getStyle().getDisplay() );
716                return; // not displayed
717            }
718
719            final float spareWidth = length - MAX_RADIUS - STROKE_WIDTH;
720            final float r = spareWidth > 6? MAX_RADIUS: MAX_RADIUS/2; // less when very short
721            getR().getBaseVal().setValue( r );
722            getCx().getBaseVal().setValue( x + length/2 + protrusion
723              - /*correction for perfect centering*/r );
724            getCy().getBaseVal().setValue( y + halfThickness );
725        }
726
727    }
728
729
730
731   // ====================================================================================
732
733
734    private final class Highlighter implements PropertyChangeHandler
735    {
736
737        Highlighter()
738        {
739            box.spool().add( new Hold()
740            {
741                final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource(
742                  PropertyChange.TYPE, /*source*/NodeV.class, Highlighter.this );
743                public void release() { hR.removeHandler(); }
744            });
745            box.spool().add( new Hold()
746            {
747                final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource(
748                  PropertyChange.TYPE, /*source*/Stage.i(), Highlighter.this );
749                public void release() { hR.removeHandler(); }
750            });
751         // relight(); // init
752         /// no need to init, is called when node set
753        }
754
755
756        public final void onPropertyChange( final PropertyChange e )
757        {
758            if( box.spool().isUnwinding() ) return;
759
760            final String name = e.propertyName();
761            final Object source = e.getSource();
762            if( source.equals( NodeV.class ))
763            {
764                if( name.equals( "deselectionGuard" )) relight();
765            }
766            else
767            {
768                assert source.equals( Stage.i() );
769                if( name.equals( "actorName" )) relight();
770            }
771        }
772
773
774        final @Warning("init call") void relight()
775        {
776            isClickable = countNode != null; // generally
777            if( isClickable && countNode.name().equals( Stage.i().getActorName() ))
778            {
779                addClassNameBaseVal( "highlit" ); // if not already added
780                deselectionGuard.guard( NodeV.this );
781            }
782            else removeClassNameBaseVal( "highlit" );
783            if( isClickable ) addClassNameBaseVal( "clickable" );
784            else removeClassNameBaseVal( "clickable" );
785        }
786
787    }
788
789
790
791   // ====================================================================================
792
793
794    private final class Mouser implements MouseOutHandler, MouseOverHandler
795    {
796        Mouser()
797        {
798            addMouseOutHandler( Mouser.this ); // no need to unregister, registry does not outlive the handler
799            addMouseOverHandler( Mouser.this ); // "
800              // Might instead add one handler to the 'svg' ancestor and catch the events
801              // as they bubble.  That requires svg's pointer-events='none' and this
802              // element's pointer-events='visible'.  The NodeV can then be resolved
803              // via event.getNativeEvent().getEventTarget()._get("__wrapper").  But then
804              // both event types (out and over) sporadically fail on Chrome (18).
805        }
806
807
808        public void onMouseOut( MouseOutEvent _e ) { sensor.out(); }
809
810
811        public void onMouseOver( MouseOverEvent _e ) { sensor.over(); }
812
813    }
814
815
816
817   // ====================================================================================
818
819
820    private final class Tracker implements ChangeHandler
821    {
822
823        Tracker( final VoteTrack track )
824        {
825            box.spool().add( new Hold()
826            {
827                final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( Change.TYPE,
828                  /*source*/track, Tracker.this );
829                public void release() { hR.removeHandler(); }
830            });
831         // run( track ); // init
832         /// no need to init, is called when node set
833        }
834
835
836        private void clearAnchor()
837        {
838            if( !isAnchor ) return;
839
840            removeClassNameBaseVal( "anchor" );
841            isAnchor = false;
842        }
843
844
845            private boolean isAnchor;
846
847
848            private void setAnchor()
849            {
850                if( isAnchor ) return;
851
852                addClassNameBaseVal( "anchor" );
853                isAnchor = true;
854            }
855
856
857        private void clearTightCycler()
858        {
859            if( !isTightCycler ) return;
860
861            removeClassNameBaseVal( "y" );
862            isTightCycler = false;
863        }
864
865
866            private void setTightCycler()
867            {
868                if( isTightCycler ) return;
869
870                addClassNameBaseVal( "y" );
871                isTightCycler = true;
872            }
873
874
875        public void onChange( final Change e ) { run( (VoteTrack)e.getSource() ); }
876
877
878        final @Warning("init call") void run( final VoteTrack track )
879        {
880            final CountNodeJS anchor = track.anchor();
881            if( anchor == null )
882            {
883                clearAnchor();
884                clearTightCycler();
885                return;
886            }
887
888            final XCastRelation place = box.place();
889            if( place == CO_VOTER )
890            {
891                if( countNode != null && countNode.dartSector() == anchor.dartSector() ) setAnchor();
892                else clearAnchor();
893                clearTightCycler();
894            }
895            else
896            {
897                clearAnchor();
898                if( isCycler() )
899                {
900                    if( place == VOTER && countNode.name().equals(anchor.candidateName())
901                     || place == CANDIDATE && anchor.name().equals(countNode.candidateName()) )
902                    {
903                        assert anchor.isVoter();
904                        assert countNode.isVoter(); /* both must cast for tight cycle, but
905                          this follows from isCycler and nominal checks above */
906                        setTightCycler();
907                        return;
908                    }
909                }
910
911                clearTightCycler();
912            }
913        }
914
915    }
916
917
918}