001package votorola.s.gwt.scene.vote; // Copyright 2011-2012, 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.JsArray;
004import com.google.gwt.jsonp.client.JsonpRequest;
005import com.google.gwt.user.client.*;
006import com.google.gwt.user.client.rpc.AsyncCallback;
007import org.vectomatic.dom.svg.*;
008import votorola.a.count.gwt.*;
009import votorola.a.web.gwt.*;
010import votorola.g.lang.*;
011import votorola.g.web.gwt.*;
012import votorola.s.gwt.scene.*;
013import votorola.s.gwt.stage.*;
014
015import static votorola.s.gwt.scene.vote.VotespaceV.MOSQUITO_BAR_DIVISOR;
016
017
018/** A semi-circular, fan-out view of voter nodes, ordered clockwise by dart sector.
019  */
020final class VoterCircle extends Circle<NodeV>
021{
022
023
024    /** Constructs a VoterCircle.
025      *
026      *     @see #inCircle()
027      *     @see #level()
028      */
029    VoterCircle( Circle<?> _inCircle, int _level, final VotespaceV vV )
030    {
031        super( _inCircle, _level, vV );
032        setVisible( false ); // prevent initial access of null candidateV
033
034      // LAYOUT  centered on candidate
035      // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
036
037      // Depth wash.
038      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
039        {
040            final float extension = 200f; // cover screen plus
041              // But not too much (2000) or it fails to close at some angles (like 3 o'clock).
042            final OMSVGPathElement path = newDepthWash( extension, -179.9f, 179.9f, vV );
043              // Nearly a full circle, completed below with square caps.  Full circle and
044              // some other angles cause erroneous translation of roughly 1 diameter.
045            path.getStyle().setSVGProperty( "stroke-linecap", "square" );
046            svgView().appendChild( path );
047        }
048
049      // Shadow backing.
050      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
051        {
052            final float extension = 2.2f; // cover voter nodes, plus
053            final float oversweep = VoterNodeV.INTER_VOTER_ANGLE * 0.8f; // cover 2 end nodes, plus
054            final OMSVGPathElement path = newDepthWash( extension,
055              VoterNodeV.angleToNode(1) - oversweep, VoterNodeV.angleToNode(20) + oversweep, vV );
056            svgView().appendChild( path );
057            path.addClassNameBaseVal( "shadowBack" );
058        }
059
060      // Voter nodes.
061      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
062        for( int dS = capacity(); dS > 0; --dS )
063        {
064            final VoterNodeV v = new VoterNodeV( vV, dS, VoterCircle.this );
065            v.setParent( VoterCircle.this );
066        }
067    }
068
069
070
071   // ------------------------------------------------------------------------------------
072
073
074    /** Recursively remodels this view and its outward views to match the currently scoped
075      * votepath.  Remodelling is initiated by the center circle.  It ends with the first
076      * voter circle that is beyond the path length.  This has the side-effect of creating
077      * an extra circle at the terminus that is always hidden.
078      */
079    void remodelOut()
080    {
081
082      // If this circle is beyond the end of the path, then hide it.
083      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
084        final Votespace model = votespaceV().model();
085        final String votepathS = model.votepath();
086        final int votepathSIndex = votepathS.length() - level();
087        if( votepathSIndex < 0 ) // this circle outside of scope, prune back
088        {
089            setVisible( false );
090            return;
091        }
092
093      // Else if visible and on the path, continue outward to next circle.
094      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
095        if( isVisible() )
096        {
097            final CountNodeJS candidate = candidateV.model();
098            if( candidate != null && votepathS.endsWith( candidateV.votepath() ))
099            {
100                outCircle().remodelOut();
101                model.syncActorToStage( candidate, candidateV.votepath() );
102                return;
103            }
104            // else hide it while remodelling:
105            setVisible( false );
106        }
107        final NodeV newCandidateV = inCircle().nodeV(
108          model.dartSector(votepathSIndex) - 1 );
109        final CountNodeJS newCandidate = newCandidateV.model();
110        model.syncActorToStage( newCandidate, newCandidateV.votepath() );
111        if( newCandidate == null ) return; // scoped to non-existant node, truncate view here
112
113        if( newCandidate.voters() == null ) // request voters
114        {
115            final String loc = newCandidate.requestVoters_loc( GWTX.stringBuilderClear() )
116              .toString();
117            final AtomicAsyncCallback<CountNodeJS, CountCache.WAPResponse> callbackA =
118              AtomicAsyncCallback.wrap( model.exchangeAtomizerS(), new CountCallback<CountNodeJS>(
119                loc, Stage.i() )
120            {
121                final boolean wasPathExtending = votespaceV().pathExtender().isPathExtending();
122                public void onFailure( final Throwable x )
123                {
124                    super.onFailure( x );
125                    if( wasPathExtending ) History.back();
126                    // Cannot sync the view with the model in its current state, so clear
127                    // away the offending scoping switch and return the model to its
128                    // previous state by navigating back.  Do this only if the user had
129                    // requested this path extension, otherwise a back navigation might
130                    // exit the app.
131                }
132                public void onSuccess( CountNodeJS _node ) { remodelOut( newCandidateV ); }
133            });
134            callbackA.init( newCandidate.requestVoters( loc, SpooledAsyncCallback.wrap(
135              votespaceV().spool(), callbackA )));
136        }
137        else remodelOut( newCandidateV ); // either way, it eventually continues outward
138    }
139
140
141
142    /** The radius of each voter circle.
143      */
144    static final float VOTER_CIRCLE_RADIUS = 11.9f;
145
146
147
148   // - C i r c l e ----------------------------------------------------------------------
149
150
151    NodeV candidateV() { return candidateV; }
152
153
154        private NodeV candidateV;
155
156
157
158    float nodularStandOff() { return 1.7f; }
159
160
161
162//// P r i v a t e ///////////////////////////////////////////////////////////////////////
163
164
165    /** Creates a filled arc with a hole in the center.
166      *
167      *   @param extension extra coverage on the outside, beyond the circle radius.
168      */
169    private OMSVGPathElement newDepthWash( final float extension,
170      final float angle1, final float angle2, final VotespaceV vV )
171    {
172        final float pxPerEm = vV.pxPerEm();
173
174        final OMSVGPathElement p = vV.document().createSVGPathElement();
175        p.addClassNameBaseVal( "depthWash" );
176        final OMSVGPathSegList d = p.getPathSegList();
177        final float holeRadius = inCircle().nodularStandOff(); // leave a hole for the candidate
178        final float radius = (VOTER_CIRCLE_RADIUS + holeRadius + extension) / 2; // of pen sweep
179        p.getStyle().setSVGProperty( "stroke-width",
180          (VOTER_CIRCLE_RADIUS - holeRadius + extension) * pxPerEm + "px" );
181        float angleRadians = (float)Math.toRadians( angle1 );
182        float x = radius * (float)Math.cos( angleRadians );
183        float y = radius * (float)Math.sin( angleRadians );
184        d.appendItem( p.createSVGPathSegMovetoAbs( x * pxPerEm, y * pxPerEm ));
185        angleRadians = (float)Math.toRadians( angle2 );
186        x = radius * (float)Math.cos( angleRadians );
187        y = radius * (float)Math.sin( angleRadians );
188        d.appendItem( p.createSVGPathSegArcAbs( x * pxPerEm, y * pxPerEm,
189          /*rx*/radius * pxPerEm, /*ry*/radius * pxPerEm, /*axial rotation of ellipse*/0,
190          /*large arc*/true, /*sweep positive*/true ));
191        return p;
192    }
193
194
195
196    /** Remodels this view to match the specified candidate view, then recursively
197      * remodels the uptree views to match the current scoping votepath.
198      */
199    private void remodelOut( final NodeV newCandidateV )
200    {
201        candidateV = newCandidateV;
202        {
203            // Ensure the candidate view is positioned first in the document, this
204            // circle's shadow backing and depth layers cover all other candidates.
205            final OMSVGElement v = candidateV.svgView();
206            v.getParentNode().appendChild( v );
207        }
208        setParent( candidateV );
209        final CountNodeJS candidate = candidateV.model();
210        setMosquitoBar( candidate.voteRegister().receiveVolume() / MOSQUITO_BAR_DIVISOR );
211
212      // Remodel this circle's voter nodes.
213      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
214        final JsArray<CountNodeJS> voters = candidate.voters();
215        for( int n = 0, nN = capacity(); n < nN; ++n )
216        {
217            final NodeV v = nodeV( n );
218            v.setModel( voters.get( n ));
219        }
220        registerPainter().repaintLater();
221
222      // Start remodelling next circle outward.  Show this circle.
223      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
224        outCircle().remodelOut();
225        setVisible( true );
226    }
227
228
229}