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}