package 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. import com.google.gwt.core.client.JsArray; import com.google.gwt.jsonp.client.JsonpRequest; import com.google.gwt.user.client.*; import com.google.gwt.user.client.rpc.AsyncCallback; import org.vectomatic.dom.svg.*; import votorola.a.count.gwt.*; import votorola.a.web.gwt.*; import votorola.g.lang.*; import votorola.g.web.gwt.*; import votorola.s.gwt.scene.*; import votorola.s.gwt.stage.*; import static votorola.s.gwt.scene.vote.VotespaceV.MOSQUITO_BAR_DIVISOR; /** A semi-circular, fan-out view of voter nodes, ordered clockwise by dart sector. */ final class VoterCircle extends Circle { /** Constructs a VoterCircle. * * @see #inCircle() * @see #level() */ VoterCircle( Circle _inCircle, int _level, final VotespaceV vV ) { super( _inCircle, _level, vV ); setVisible( false ); // prevent initial access of null candidateV // LAYOUT centered on candidate // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = // Depth wash. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final float extension = 200f; // cover screen plus // But not too much (2000) or it fails to close at some angles (like 3 o'clock). final OMSVGPathElement path = newDepthWash( extension, -179.9f, 179.9f, vV ); // Nearly a full circle, completed below with square caps. Full circle and // some other angles cause erroneous translation of roughly 1 diameter. path.getStyle().setSVGProperty( "stroke-linecap", "square" ); svgView().appendChild( path ); } // Shadow backing. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final float extension = 2.2f; // cover voter nodes, plus final float oversweep = VoterNodeV.INTER_VOTER_ANGLE * 0.8f; // cover 2 end nodes, plus final OMSVGPathElement path = newDepthWash( extension, VoterNodeV.angleToNode(1) - oversweep, VoterNodeV.angleToNode(20) + oversweep, vV ); svgView().appendChild( path ); path.addClassNameBaseVal( "shadowBack" ); } // Voter nodes. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - for( int dS = capacity(); dS > 0; --dS ) { final VoterNodeV v = new VoterNodeV( vV, dS, VoterCircle.this ); v.setParent( VoterCircle.this ); } } // ------------------------------------------------------------------------------------ /** Recursively remodels this view and its outward views to match the currently scoped * votepath. Remodelling is initiated by the center circle. It ends with the first * voter circle that is beyond the path length. This has the side-effect of creating * an extra circle at the terminus that is always hidden. */ void remodelOut() { // If this circle is beyond the end of the path, then hide it. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final Votespace model = votespaceV().model(); final String votepathS = model.votepath(); final int votepathSIndex = votepathS.length() - level(); if( votepathSIndex < 0 ) // this circle outside of scope, prune back { setVisible( false ); return; } // Else if visible and on the path, continue outward to next circle. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( isVisible() ) { final CountNodeJS candidate = candidateV.model(); if( candidate != null && votepathS.endsWith( candidateV.votepath() )) { outCircle().remodelOut(); model.syncActorToStage( candidate, candidateV.votepath() ); return; } // else hide it while remodelling: setVisible( false ); } final NodeV newCandidateV = inCircle().nodeV( model.dartSector(votepathSIndex) - 1 ); final CountNodeJS newCandidate = newCandidateV.model(); model.syncActorToStage( newCandidate, newCandidateV.votepath() ); if( newCandidate == null ) return; // scoped to non-existant node, truncate view here if( newCandidate.voters() == null ) // request voters { final String loc = newCandidate.requestVoters_loc( GWTX.stringBuilderClear() ) .toString(); final AtomicAsyncCallback callbackA = AtomicAsyncCallback.wrap( model.exchangeAtomizerS(), new CountCallback( loc, Stage.i() ) { final boolean wasPathExtending = votespaceV().pathExtender().isPathExtending(); public void onFailure( final Throwable x ) { super.onFailure( x ); if( wasPathExtending ) History.back(); // Cannot sync the view with the model in its current state, so clear // away the offending scoping switch and return the model to its // previous state by navigating back. Do this only if the user had // requested this path extension, otherwise a back navigation might // exit the app. } public void onSuccess( CountNodeJS _node ) { remodelOut( newCandidateV ); } }); callbackA.init( newCandidate.requestVoters( loc, SpooledAsyncCallback.wrap( votespaceV().spool(), callbackA ))); } else remodelOut( newCandidateV ); // either way, it eventually continues outward } /** The radius of each voter circle. */ static final float VOTER_CIRCLE_RADIUS = 11.9f; // - C i r c l e ---------------------------------------------------------------------- NodeV candidateV() { return candidateV; } private NodeV candidateV; float nodularStandOff() { return 1.7f; } //// P r i v a t e /////////////////////////////////////////////////////////////////////// /** Creates a filled arc with a hole in the center. * * @param extension extra coverage on the outside, beyond the circle radius. */ private OMSVGPathElement newDepthWash( final float extension, final float angle1, final float angle2, final VotespaceV vV ) { final float pxPerEm = vV.pxPerEm(); final OMSVGPathElement p = vV.document().createSVGPathElement(); p.addClassNameBaseVal( "depthWash" ); final OMSVGPathSegList d = p.getPathSegList(); final float holeRadius = inCircle().nodularStandOff(); // leave a hole for the candidate final float radius = (VOTER_CIRCLE_RADIUS + holeRadius + extension) / 2; // of pen sweep p.getStyle().setSVGProperty( "stroke-width", (VOTER_CIRCLE_RADIUS - holeRadius + extension) * pxPerEm + "px" ); float angleRadians = (float)Math.toRadians( angle1 ); float x = radius * (float)Math.cos( angleRadians ); float y = radius * (float)Math.sin( angleRadians ); d.appendItem( p.createSVGPathSegMovetoAbs( x * pxPerEm, y * pxPerEm )); angleRadians = (float)Math.toRadians( angle2 ); x = radius * (float)Math.cos( angleRadians ); y = radius * (float)Math.sin( angleRadians ); d.appendItem( p.createSVGPathSegArcAbs( x * pxPerEm, y * pxPerEm, /*rx*/radius * pxPerEm, /*ry*/radius * pxPerEm, /*axial rotation of ellipse*/0, /*large arc*/true, /*sweep positive*/true )); return p; } /** Remodels this view to match the specified candidate view, then recursively * remodels the uptree views to match the current scoping votepath. */ private void remodelOut( final NodeV newCandidateV ) { candidateV = newCandidateV; { // Ensure the candidate view is positioned first in the document, this // circle's shadow backing and depth layers cover all other candidates. final OMSVGElement v = candidateV.svgView(); v.getParentNode().appendChild( v ); } setParent( candidateV ); final CountNodeJS candidate = candidateV.model(); setMosquitoBar( candidate.voteRegister().receiveVolume() / MOSQUITO_BAR_DIVISOR ); // Remodel this circle's voter nodes. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final JsArray voters = candidate.voters(); for( int n = 0, nN = capacity(); n < nN; ++n ) { final NodeV v = nodeV( n ); v.setModel( voters.get( n )); } registerPainter().repaintLater(); // Start remodelling next circle outward. Show this circle. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - outCircle().remodelOut(); setVisible( true ); } }