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.*; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HasHandlers; import com.google.web.bindery.event.shared.HandlerRegistration; import votorola.a.count.gwt.*; import votorola.g.hold.*; import votorola.g.lang.*; import votorola.g.util.Holder; import votorola.g.web.gwt.*; import votorola.g.web.gwt.event.*; import votorola.s.gwt.stage.*; /** A area of votespace centered on * one person ({@linkplain #anchor() the anchor}) and his/her direct {@linkplain * votorola.a.count.XCastRelation cast relations}. All changes to track state are * signalled by {@linkplain Change change events} on the {@linkplain GWTX#bus() bus}. * Event bursts are coalesced such that a single event is fired regardless of the number * of state variables involved. * * @see VoteTrackV */ public final class VoteTrack implements HasHandlers, Hold, Track { /** Constructs a VoteTrack. Call {@linkplain #release release}() when done with it. */ public VoteTrack() { Stage.i().addInitializer( new TheatreInitializer() // auto-removed { // cf. s.gwt.stage.poll.Polltrack // Early referrer resolution. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - public void initTo( Stage _s ) {} // public void initTo( final Stage s, final TheatrePage r ) { initR( s, r ); } /// but let init complete for sake of initR guards public void initTo( final Stage s, final TheatrePage r ) { initToReferrer = r; } private TheatrePage initToReferrer; public void initToComplete( final Stage s, final boolean rPending ) { initComplete( s, rPending ); final TheatrePage r = initToReferrer; if( r == null ) return; // no referrer state to transfer assert !rPending; initR( s, r ); } // Late referrer resolution. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - public void initFrom( Stage _s, boolean _rPending ) {} public void initFromComplete( final Stage s, final boolean rPending ) { initComplete( s, rPending ); if( rPending ) initFromActorName = s.getActorName(); // continues at initUltimately } private String initFromActorName; // as restored from single page persistence public void initUltimately( final Stage s, final TheatrePage r ) { if( r == null ) return; // no referrer state to transfer if( !ObjectX.nullEquals( s.getActorName(), initFromActorName )) return; // changed by user or prop, do not clobber initR( s, r ); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - public void initComplete( final Stage s, final boolean rPending ) { initWaitSpool.unwind(); new Tracker( s ); } private void initR( final Stage s, final TheatrePage r ) { if( r == null ) return; // no referrer state to transfer final String rActorName = r.getActorName(); if( rActorName == null ) return; // no actor on referrer final boolean willBePolless = s.getPollName() == null && r.getPollName() == null; // will be, that is, after prop such as polltrack carries it across if( s.getDefaultActorName() != null && willBePolless ) return; // This is actor's page because default actor staged. Don't carry an // actor (like a different actor) here when no poll will be staged. // Vote track is useless without poll and probably has no useful // information to preserve. (The specific motivation here is to avoid // odd behaviour of actor mysteriously carried to another's home page.) final JavaScriptObject voc = WindowX.js()._get( "voc" ); if( voc != null ) { final String pageJClass = voc._getString( "pageJClass" ); // per a.web.wic.VPageHTML if( "votorola.s.wic.count.WP_Votespace".equals(pageJClass) || "votorola.s.wic.count.WP_Rank".equals(pageJClass) ) return; // per s.gwt.wic.CountIn. These have PositionPager, cannot tolerate // actor init other than own } Stage.setActorName( rActorName ); } }); } /** Returns the {@linkplain Stage#tracks() staged instance} of VoteTrack, or null if * none is staged. */ public static VoteTrack i( final Stage stage ) { for( Track t: stage.tracks() ) if( t instanceof VoteTrack ) return (VoteTrack)t; return null; } /** A spool that unwinds just before this track commences initializing, which it does * on "{@linkplain TheatreInitializerC#initComplete(Stage, boolean) init complete}". * Initializers that need to run after the stage is ready and before the first event * is fired from this track should wrap themselves here. */ Spool initWaitSpool() { return initWaitSpool; } private final Spool initWaitSpool = new Spool1(); // ------------------------------------------------------------------------------------ /** The count node of the default or current actor complete with {@linkplain * CountNodeJS#voters() voters}, or null if either no anchor is {@linkplain * #anchorName(Stage) expected}, or the node is yet unknown. The anchor may either * be {@linkplain #toPin(Stage) pinned} or left free to move in votespace. */ public CountNodeJS anchor() { return anchor; } private CountNodeJS anchor; /** A constant holder for the anchor node. */ Holder anchorHolder() { return anchorHolder; } private final Holder anchorHolder = new Holder() { public CountNodeJS get() { return anchor; } }; private void retrackAnchor( final Stage stage ) // only called when count not null { final String anchorName = anchorName( stage ); if( anchorName == null ) clearAnchor(); else if( anchor != null && anchor.count() == count && anchor.name().equals(anchorName) ) return; else // new anchor { final CountJS.NodeRequestRecord record = count.nodeRequestRecord( anchorName ); final CountNodeJS anchorNew; if( CountJS.isNodeRequestComplete( record )) anchorNew = record.node(); else // not fully requested { final String loc = count.requestNode_loc( anchorName, GWTX.stringBuilderClear() ).toString(); final AtomicAsyncCallback callbackA = AtomicAsyncCallback.wrap( exchangeAtomizer, new CountCallback( loc, stage ) { public void onSuccess( final CountNodeJS aNew ) { forceAnchor( aNew ); } // no need to clear on failure, already cleared below on request }); callbackA.init( count.requestNode( loc, anchorName, SpooledAsyncCallback.wrap(spool,callbackA) )); anchorNew = null; } if( anchorNew == null ) clearAnchor( record ); else forceAnchor( anchorNew ); } } private void clearAnchor() { clearAnchor( null ); } // and dependents private void clearAnchor( final CountJS.NodeRequestRecord record ) // and maybe dependents { String expectedCandidateName = null; if( record != null ) { final CountNodeJS newAnchor = record.node(); if( newAnchor != null ) expectedCandidateName = newAnchor.candidateName(); // anchor being cleared until new anchor's voter's are fetched } if( anchor != null ) { anchor = null; gun.schedule(); } clearNonMatchingCandidate( expectedCandidateName ); // do not clear candidate unecessarily, as it will disable peers and cause // peers view to disappear and reappear for no good reason } private void forceAnchor( final CountNodeJS anchorNew ) // unguarded setter { assert anchorNew != null; anchor = anchorNew; gun.schedule(); retrackCandidate(); } /** Returns the mailish username of the person who should be {@linkplain #anchor() * anchor}, or null if nobody should be. */ static String anchorName( final Stage stage ) { String name = stage.getDefaultActorName(); if( name == null ) name = stage.getActorName(); return name; } /** The count node of the {@linkplain #anchor() anchor}'s candidate complete with * {@linkplain CountNodeJS#voters() voters}, or null if either the anchor has no * candidate, or the node is yet unknown. */ public CountNodeJS candidate() { return candidate; } private CountNodeJS candidate; /** A constant holder for the variable candidate. */ Holder candidateHolder() { return candidateHolder; } private final Holder candidateHolder = new Holder() { public CountNodeJS get() { return candidate; } }; private void retrackCandidate() // only called when anchor and count not null { if( !anchor.isVoter() ) { clearCandidate(); return; } final String candidateName = anchor.candidateName(); if( candidate != null && candidate.count() == count && candidate.name().equals(candidateName) ) return; else // new candidate { final CountJS.NodeRequestRecord record = count.nodeRequestRecord( candidateName ); final CountNodeJS candidateNew; if( CountJS.isNodeRequestComplete( record )) candidateNew = record.node(); else // not fully requested { final String loc = count.requestNode_loc( candidateName, GWTX.stringBuilderClear() ).toString(); final AtomicAsyncCallback callbackA = AtomicAsyncCallback.wrap( exchangeAtomizer, new CountCallback( loc, Stage.i() ) { public void onSuccess( final CountNodeJS cNew ) { forceCandidate( cNew ); } // no need to clear on failure, already cleared below on request }); callbackA.init( count.requestNode( loc, candidateName, SpooledAsyncCallback.wrap(spool,callbackA) )); candidateNew = null; } if( candidateNew == null ) clearCandidate(); else forceCandidate( candidateNew ); } } private void clearCandidate() // and dependents { if( candidate != null ) { candidate = null; hasTightCycle = false; gun.schedule(); } clearDownstream(); } private void clearNonMatchingCandidate( final String name ) // or null { if( candidate == null ) return; if( name == null || !name.equals( candidate.name() )) clearCandidate(); } private void forceCandidate( final CountNodeJS candidateNew ) // unguarded setter { assert candidateNew != null; candidate = candidateNew; hasTightCycle = anchor.name().equals(candidate.candidateName()) && candidate.isVoter(); gun.schedule(); retrackDownstream(); } /** The tracked count, or null if it is yet unknown. */ public CountJS count() { return count; } private CountJS count; private void retrackCount( final Stage stage ) // and everything else { final String pollName = stage.getPollName(); if( pollName == null ) clearCount(); else if( count != null && count.pollName().equals( pollName )) retrackAnchor( stage ); else // new poll { final CountCache countCache = CountCache.i(); final CountCache.RequestRecord record = countCache.requestRecord( pollName ); final CountJS countNew; if( record == null ) // not previously requested { final String loc = CountCache.requestCount_loc( pollName, GWTX.stringBuilderClear() ).toString(); final AtomicAsyncCallback callbackA = AtomicAsyncCallback.wrap( exchangeAtomizer, new CountCallback( loc, stage ) { public void onSuccess( final CountJS cNew ) { forceCount( cNew ); } // no need to clear on failure, already cleared below on request }); callbackA.init( countCache.requestCount( pollName, loc, SpooledAsyncCallback.wrap(spool,callbackA) )); countNew = null; } else countNew = record.count(); if( countNew == null ) clearCount(); else forceCount( countNew ); } } private void clearCount() { if( count == null ) { assert anchor == null && candidate == null; return; } count = null; gun.schedule(); clearAnchor(); } private void forceCount( final CountJS countNew ) // unguarded setter { assert countNew != null; count = countNew; gun.schedule(); retrackAnchor( Stage.i() ); } /** The count node of the {@linkplain #candidate() candidate}'s candidate complete * with {@linkplain CountNodeJS#voters() voters}, or null if either the candidate has * no candidate, or the node is yet unknown. */ public CountNodeJS downstream() { return downstream; } private CountNodeJS downstream; /** A constant holder for the variable downstream node. */ Holder downstreamHolder() { return downstreamHolder; } private final Holder downstreamHolder = new Holder() { public CountNodeJS get() { return downstream; } }; private void retrackDownstream() // only called when anchor, count and candidate not null { if( !candidate.isVoter() ) { clearDownstream(); return; } final String downstreamName = candidate.candidateName(); if( downstream != null && downstream.count() == count && downstream.name().equals(downstreamName) ) return; else // new downstream { final CountJS.NodeRequestRecord record = count.nodeRequestRecord( downstreamName ); final CountNodeJS downstreamNew; if( CountJS.isNodeRequestComplete( record )) downstreamNew = record.node(); else // not fully requested { final String loc = count.requestNode_loc( downstreamName, GWTX.stringBuilderClear() ).toString(); final AtomicAsyncCallback callbackA = AtomicAsyncCallback.wrap( exchangeAtomizer, new CountCallback( loc, Stage.i() ) { public void onSuccess( final CountNodeJS dNew ) { forceDownstream( dNew ); } // no need to clear on failure, already cleared below on request }); callbackA.init( count.requestNode( loc, downstreamName, SpooledAsyncCallback.wrap(spool,callbackA) )); downstreamNew = null; } if( downstreamNew == null ) clearDownstream(); else forceDownstream( downstreamNew ); } } private void clearDownstream() { if( downstream == null ) return; downstream = null; gun.schedule(); } private void forceDownstream( final CountNodeJS downstreamNew ) // unguarded setter { assert downstreamNew != null; downstream = downstreamNew; gun.schedule(); } /** True if the {@linkplain #candidate() candidate} and anchor are in a tight cycle; * false if either there is no candidate, or its node is yet unknown. When true the * candidate will appear not only as the candidate, but also as a {@linkplain * #voters() voter}. * * @see tight cycle */ // per INLDOC public boolean hasTightCycle() { return hasTightCycle; } private boolean hasTightCycle; /** The board of peers, meaning either the anchor node's {@linkplain * votorola.a.count.XCastRelation#CO_VOTER co-voters} or the {@linkplain * votorola.a.count.XCastRelation#CO_BASE base co-candidates}. The state of the * board is maintained according to the following conditions applied in order: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
ConditionContent
(a)no count{@linkplain Board#isEnabled() disabled}
(b)no anchornodes are {@linkplain CountJS#baseCandidates() base candidates}
(c)anchor pending * but named in candidate's {@linkplain CountNodeJS#voters() voters}nodes are candidate's voters
(d)anchor pending but name matches root candidatenodes are base candidates
(e)anchor pendingdisabled
(f)anchor is root candidatenodes are base candidates
(g)no candidatenodes are base candidates, * anchor is {@linkplain Board#mosquito() mosquito} orphan
(h)candidate pendingdisabled
(i)defaultnodes are candidate's voters
* *

The {@linkplain #anchor() anchor} (if any) is always included, either as a * {@linkplain PeerBoard#node(int) dart-sectored node}, or as a {@linkplain * PeerBoard#mosquito() mosquito}.

*/ public PeerBoard peers() { return peers; } private final PeerBoard peers = new PeerBoard( VoteTrack.this ); private void syncPeers() { /* a */if( count == null ) { peers.disable(); return; } final String anchorName = anchorName( Stage.i() ); if( anchorName == null ) { /* b */ peers.enable( null, count.baseCandidates(), /*isEndBoard*/true ); } else if( anchor == null ) // pending { JsArray coNodes; if( candidate != null ) { /* c */ coNodes = candidate.voters(); if( CountNodeJS.findNode(anchorName,coNodes) != null ) { peers.enable( null, coNodes, /*isEndBoard*/false ); return; } } /* d */ coNodes = count.baseCandidates(); final CountNodeJS anchorAtEnd = CountNodeJS.findNode( anchorName, coNodes ); if( anchorAtEnd != null && !anchorAtEnd.isVoter() ) { peers.enable( null, coNodes, /*isEndBoard*/true ); return; } /* e */ peers.disable(); } else if( !anchor.isVoter() ) { /* f */ if( anchor.directVoterCount() > 0L ) { peers.enable( unsectoredAnchor(), count.baseCandidates(), /*isEndBoard*/true ); } /* g */ else peers.enable( anchor, count.baseCandidates(), /*isEndBoard*/true ); } /* h */else if( candidate == null ) peers.disable(); /* i */else peers.enable( unsectoredAnchor(), candidate.voters(), /*isEndBoard*/false ); } /** Answers whether the {@linkplain #anchor() anchor} should currently be pinned to a * single node in votespace, or free to follow the {@linkplain Stage#getActorName() * staged actor}. It should be pinned when a {@linkplain Stage#getDefaultActorName() * default actor} is set, otherwise it should be free. */ static boolean toPin( final Stage stage ) { return stage.getDefaultActorName() != null; } /** The board of the anchor node's direct voters. If there is no anchor node, then * this board is disabled. If the track has a {@linkplain #hasTightCycle tight * cycle}, then the candidate is included as a {@linkplain Board#node(int) sectored * node} or {@linkplain Board#mosquito() mosquito}. */ public Board voters() { return voters; } private final Board voters = new Board( VoteTrack.this ); private void syncVoters() { if( anchor == null ) voters.disable(); else { CountNodeJS mosquito = hasTightCycle && candidate.dartSector() == 0? candidate: null; voters.enable( mosquito, anchor.voters(), /*isEndBoard*/false ); } } // - H a s - H a n d l e r s ---------------------------------------------------------- public void fireEvent( final GwtEvent e ) { assert e instanceof Change; syncPeers(); syncVoters(); GWTX.i().bus().fireEventFromSource( e, VoteTrack.this ); } // - H o l d -------------------------------------------------------------------------- public void release() { spool.unwind(); } // - T r a c k ------------------------------------------------------------------------ public VoteTrackV newView( StageV _stageV ) { return new VoteTrackV( VoteTrack.this, /*isBottomFixed*/false ); } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private final JSONPAtomizer exchangeAtomizer = new JSONPAtomizer(); private final Change.CoalescingGun gun = new Change.CoalescingGun( /*source*/VoteTrack.this, CoalescingSchedulerS.DEFERRED ); // VoteTrackV.Animator depends on delay private final Spool spool = new Spool1(); private CountNodeJS unsectoredAnchor() { return anchor.dartSector() == 0? anchor: null; } // ==================================================================================== private final class Tracker implements PropertyChangeHandler, Scheduler.ScheduledCommand { // cf. s.gwt.scene.vote.Votespace.Scoper Tracker( final Stage stage ) { spool.add( new Hold() { final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( PropertyChange.TYPE, /*source*/stage, Tracker.this ); public void release() { hR.removeHandler(); } }); retrackCount( stage ); // init } private final CoalescingSchedulerS coalescer = new CoalescingSchedulerS( CoalescingSchedulerS.FINALLY, Tracker.this ); // - P r o p e r t y - C h a n g e - H a n d l e r -------------------------------- public final void onPropertyChange( final PropertyChange e ) { if( spool.isUnwinding() ) return; final String name = e.propertyName(); if( name.equals("pollName") || name.equals("actorName") ) coalescer.schedule(); // which later calls retrackCount } // - S c h e d u l e r . S c h e d u l e d - C o m m a n d ------------------------ public final void execute() { retrackCount( Stage.i() ); } } }