001package votorola.s.wic.count; // Copyright 2009-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 java.io.*; 004import java.sql.*; 005import java.text.*; 006import java.util.*; 007import java.util.Date; // over java.sql.Date 008import javax.script.*; 009import javax.xml.stream.XMLStreamException; 010import org.apache.wicket.*; 011import org.apache.wicket.behavior.AttributeAppender; 012import org.apache.wicket.markup.html.*; 013import org.apache.wicket.markup.html.basic.*; 014import org.apache.wicket.markup.html.form.*; 015import org.apache.wicket.markup.html.link.*; 016import org.apache.wicket.markup.html.panel.*; 017import org.apache.wicket.markup.repeater.*; 018import org.apache.wicket.model.*; 019import org.apache.wicket.protocol.http.PageExpiredException; 020import org.apache.wicket.request.cycle.*; 021import org.apache.wicket.request.mapper.parameter.PageParameters; 022import org.apache.wicket.util.string.StringValue; 023import votorola.a.*; 024import votorola.a.count.*; 025import votorola.a.position.*; 026import votorola.a.voter.*; 027import votorola.a.web.wic.*; 028import votorola.a.web.wic.authen.*; 029import votorola.g.*; 030import votorola.g.lang.*; 031import votorola.g.locale.*; 032import votorola.g.mail.*; 033import votorola.g.web.wic.*; 034import votorola.g.text.*; 035 036import static votorola.a.count.CountNode.DART_SECTOR_MAX; 037import static votorola.a.voter.IDPair.NOBODY; 038 039 040/** A view of the vote structure for a poll. A Crossforum Theatre {@linkplain 041 * votorola.s.gwt.stage.StageV stage view} tops the page. The bulk of the page is 042 * occupied by static HTML including a voting control and a navigable view of the vote 043 * structure in cascading table form. For example: 044 * 045 * <blockquote><code><a href='http://reluk.ca:8080/v/w/Votespace?p=G!p!sandbox' target='_top'>http://reluk.ca:8080/v/w/Votespace?p=G!p!sandbox</a></code></blockquote> 046 * 047 * <p>The particular poll is specified by query parameter 'p'. Query parameters for this 048 * page are:</p> 049 * 050 * <table class='definition' style='margin-left:1em'> 051 * <tr> 052 * <th class='key'>Key</th> <!-- adding params? add to pKey too --> 053 * <th>Value</th> 054 * <th>Default</th> 055 * <th>Recall</th> 056 * </tr> 057 * <tr><td class='key'>p</td> 058 * 059 * <td>The name of the <a href='http://reluk.ca/w/Category:Poll' target='_top'>poll</a>. 060 * Slash characters (/) are technically not allowed here 061 * and may therefore be encoded as exclamation marks (!).</td> 062 * 063 * <td>Null, resulting in a 303 (see other) redirect that fills in the name of 064 * the {@linkplain Poll#TEST_POLL_NAME test poll}.</td> 065 * 066 * <td>yes</td> 067 * 068 * </tr> 069 * <tr><td class='key'>recallRedirect</td> 070 * 071 * <td>Recall parameters and redirect. The value is a set of recallable 072 * parameter names, separated by vertical bars (e.g. 'p|u'). The server attempts 073 * to recall the values from the context of recent requests, then responds with a 074 * corrected URL in the form of a 303 redirect. Parameters that are specified 075 * elsewhere in the request are not recalled in any case, and the values are 076 * passed as specified into the corrected URL.</td> 077 * 078 * <td>Null, doing no redirection.</td> 079 * 080 * <td>no</td> 081 * 082 * </tr> 083 * <tr><td class='key'>u</td> 084 * 085 * <td>The {@linkplain IDPair#username() username} of the person at the top of 086 * the vote path. Incompatible with parameter 'v'; specify one or the 087 * other.</td> 088 * 089 * <td>Null, specifying no particular person.</td> 090 * 091 * <td>yes</td> 092 * 093 * </tr> 094 * <tr><td class='key'>v</td> 095 * 096 * <td>The {@linkplain IDPair#email() email address} of the person at the top of 097 * the vote path. Incompatible with parameter 'u'; specify one or the 098 * other.</td> 099 * 100 * <td>Null, specifying no particular person.</td> 101 * 102 * <td>yes</td> 103 * 104 * </tr> 105 * <tr><td class='key'>vCor</td> 106 * 107 * <td>Whether to correct the results for any vote shift of the user's since the 108 * last reported count. A value of 'y' corrects the results, while 'n' leaves 109 * them uncorrected.</td> 110 * 111 * <td>'y'</td> 112 * 113 * <td>no</td> 114 * 115 * </tr> 116 * </table> 117 * 118 * @see <a href='../../../../../../s/wic/count/WP_Votespace.html' target='_top'>WP_Votespace.html</a> 119 */ 120 @ThreadRestricted("wicket") @org.apache.wicket.devutils.stateless.StatelessComponent 121public final class WP_Votespace extends VPageHTML implements TabbedPage, VoterPage 122{ 123 124 /** Constructs a WP_Votespace. 125 */ 126 public WP_Votespace( final PageParameters pP ) throws IOException, ScriptException, SQLException, 127 XMLStreamException // bookmarkable page iff constructor public & (default|PageParameter) 128 { 129 super( pP ); 130 final VRequestCycle cycle = VRequestCycle.get(); 131 maybeRecallRedirect( WP_Votespace.class, pP, cycle ); 132 133 final PollService poll = WP_Poll.ensurePoll( WP_Votespace.class, pP, cycle ); 134 pollName = poll.name(); 135 final VSession session = VSession.get(); 136 session.scopePoll().setLastName( pollName ); 137 setPageIcon( cycle.vRequest().getContextPath() + "/count/WP_Votespace/icon16.png" ); 138 voterIDPair = VoterPage.U.idPairOrNobodyFor( pP ); 139 session.scopeVoterPage().setLastIDPair( voterIDPair ); 140 141 // Write glue for the GWT stage module of Crossforum Theatre 142 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 143 final VoteServer vS = poll.vsRun().voteServer(); 144 final VSession.User userOrNull = session.user(); 145 { 146 final StringBuilder b = WC_Stage.appendLeader( userOrNull, vS, cycle ); 147 148 // s_gwt_stage_Stage_init, per WC_Stage below 149 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 150 b.append( 151 "voGWTConfig.s_gwt_stage_Stage_init = function()" 152 + "{" ); 153 b.append( 154 "s_gwt_stage_Stage_setActorName( " ); 155 // rather than setDefaultActorName, which would disable absolute 156 // deselection (the default being selected instead). We need 157 // absolute deselection of base candidates for consistency with the 158 // votespace scene. votorola.s.gwt.wic.PositionPager could detect a 159 // deselection attempt that was disabled by a default setting if it 160 // listened for the masked event, but that would be more complicated. 161 // Changing? change also votorola.s.gwt.wic.CountIn.moduleLoad. 162 if( NOBODY.equals( voterIDPair )) b.append( "null );" ); 163 else b.append( '\'' ).append( voterIDPair.username() ).append( "' );" ); 164 b.append( 165 "s_gwt_stage_Stage_setDefaultPollName( '" ); 166 b.append( pollName ).append( "' );" 167 + "};" ); 168 169 // ` ` ` 170 add( new WC_Stage( "stage", "votorola.s.gwt.wic.CountIn", b, cycle )); 171 } 172 173 // Render view 174 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 175 add( new WC_NavigationHead( "navHead", WP_Votespace.this, cycle )); 176 add( new WC_WGLogo( "wgLogo", poll.wgLogoImageLocation(), poll.wgLogoLinkTarget(), cycle )); 177 { 178 final String mapPageName = poll.divisionSmallMapPageName(); 179 add( mapPageName == null? newNullComponent( "divisionSmallMap" ): 180 new WC_DivisionSmallMap( "divisionSmallMap", poll.divisionPageName(), mapPageName, 181 cycle )); 182 } 183 add( new WC_NavPile( "navPile", navTab(cycle), cycle )); 184 try 185 { 186 init_content( vS, poll, userOrNull, cycle ); 187 } 188 catch( Exception x ) { throw VotorolaRuntimeException.castOrWrapped( x ); } 189 setCacheable( true ); 190 } 191 192 193 194 @SuppressWarnings("deprecation") // IDPair.isFromEmail() 195 private void init_content( final VoteServer vS, final PollService pollOrNull, 196 final VSession.User userOrNull, final VRequestCycle cycle ) 197 throws IOException, ScriptException, SQLException, XMLStreamException 198 { 199 final BundleFormatter bunW = cycle.bunW(); 200 201 // POLL AND TITLING 202 // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 203 final Model<String> titleModel = new Model<String>( bunW.l( 204 "s.wic.count.WP_Votespace.title" )); 205 add( new Label( "title", titleModel )); 206 if( pollOrNull == null ) 207 { 208 assert false: "poll is never null"; // FIX clean up 209 add( newNullComponent( "contentPoll" )); 210 return; 211 } 212 213 final PollService poll = pollOrNull; 214 titleModel.setObject( titleModel.getObject() + " - " + poll.name() ); 215 216 final Fragment yPoll = newBodyOnlyFragment( "contentPoll", "contentPollFrag", 217 WP_Votespace.this ); 218 yPoll.add( new Label( "hName", poll.name() )); 219 { 220 final String displayTitle = poll.displayTitle(); 221 if( displayTitle == null ) yPoll.add( newNullComponent( "hDisplayTitle" )); 222 else yPoll.add( new Label( "hDisplayTitle", ": " + displayTitle )); 223 } 224 add( yPoll ); 225 226 // CANDIDATE NAVIGATION AND VOTING CONTROLS 227 // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 228 final CorrectableCount count; 229 { 230 final Count c = poll.countToReportT(); 231 count = c == null? null: new CorrectableCount( c ); 232 } 233 234 final BundleFormatter bunA = cycle.bunA(); 235 final String nobodyString = bunA.l( "a.count.nobodyEmailPlaceholder" ); 236 final boolean isVotingEnabled; 237 final String userEmail; 238 if( userOrNull == null ) 239 { 240 isVotingEnabled = false; 241 userEmail = null; 242 currentVote = new Vote( NOBODY.email() ); 243 } 244 else 245 { 246 isVotingEnabled = true; 247 userEmail = userOrNull.email(); 248 currentVote = new Vote( userEmail, poll.voterInputTable() ); 249 } 250 newVote = currentVote.clone(); 251 { 252 final IDPair candidate; 253 if( voterEmail().equals( userEmail )) 254 { 255 final String email = currentVote.getCandidateEmail(); 256 candidate = email == null? NOBODY: new IDPair( email, IDPair.toUsername(email), 257 /*isFromEmail*/voterIDPair.isFromEmail() ); // force to form as specified for voter 258 } 259 else if( NOBODY.equals( voterIDPair )) candidate = NOBODY; 260 else candidate = voterIDPair; 261 setNewCandidate( candidate ); // and thence newVote 262 } 263 264 final CandidateForm candidateForm = new CandidateForm(); 265 yPoll.add( candidateForm ); 266 { 267 final TextField<IDPair> field = new TextField<IDPair>( "otherUID" ); 268 candidateForm.add( field ); 269 270 field.setModel( new PropertyModel<IDPair>( WP_Votespace.this, "newCandidate" ) 271 { 272 public @Override IDPair getObject() 273 { 274 final IDPair o = super.getObject(); 275 return NOBODY.equals(o)? null: o; 276 } 277 public @Override void setObject( final IDPair o ) 278 { 279 super.setObject( o == null? NOBODY: o ); 280 } 281 }); 282 invalidStyled( field ); 283 IDPairConverter.setMaxLength_Type( field ); 284 285 if( isVotingEnabled ) candidateForm.add( newNullComponent( "loginLink" )); 286 else 287 { 288 final WC_LoginLink link = new WC_LoginLink( "loginLink", WP_Votespace.this, 289 bunW.l( "s.wic.count.WP_Votespace.login" )); 290 candidateForm.add( link ); 291 292 field.setEnabled( count != null ); // no need of field, if all the buttons are disabled 293 } 294 } 295 { 296 final Button button = new Button( "go" ); 297 button.add( AttributeModifier.replace( "value", 298 bunW.l( "s.wic.count.WP_Votespace.candidateGo" ))); 299 candidateForm.add( button ); 300 301 button.setEnabled( count != null ); // no point in navigating anywhere, there is no view 302 } 303 { 304 final Button button = new Button( "vote" ); 305 button.add( AttributeModifier.replace( "value", 306 bunW.l( "s.wic.count.WP_Votespace.candidateVote" ))); 307 button.setEnabled( isVotingEnabled ); 308 candidateForm.add( button ); 309 } 310 { 311 final Button button = new Button( "unvote" ); 312 button.add( AttributeModifier.replace( "value", 313 bunW.l( "s.wic.count.WP_Votespace.candidateUnvote" ))); 314 button.setEnabled( isVotingEnabled && currentVote.getCandidateEmail() != null ); 315 candidateForm.add( button ); 316 } 317 318 // Feedback messages 319 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 320 candidateForm.add( new WC_Feedback( "feedback" )); 321 322 // COUNT 323 // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 324 // Summary view of count and user's vote (candidate detail). It is independent of 325 // the vote path shown in the main cascade view (below). It therefore remains fixed 326 // in height during path navigation, not causing the cascade view to shift 327 // vertically. Keep it so. 328 329 final Fragment yCount; 330 final MarkupContainer candidateDetail; 331 if( count == null ) 332 { 333 yCount = new Fragment( "contentCount", "contentCountNullFrag", 334 WP_Votespace.this ); 335 yCount.add( new Label( "explanation", bunA.l( "a.count.noResultsToReport" ))); 336 yPoll.add( yCount ); 337 338 if( isVotingEnabled ) 339 { 340 candidateDetail = new Fragment( "candidateDetail", "candidateLyCnFrag", 341 WP_Votespace.this ); 342 { 343 final String candidateEmail = currentVote.getCandidateEmail(); 344 final Label label = new Label("candidate", 345 bunW.l( "s.wic.count.WP_Votespace.candidateLyCn", 346 candidateEmail == null? nobodyString: 347 IDPair.toUsername( candidateEmail ))); 348 label.setRenderBodyOnly( true ); 349 candidateDetail.add( label ); 350 } 351 } 352 else candidateDetail = newNullComponent( "candidateDetail" ); 353 candidateForm.add( candidateDetail ); 354 return; 355 } 356 357 yCount = new Fragment( "contentCount", "contentCountFrag", WP_Votespace.this ); 358 yPoll.add( yCount ); 359 360 final CountTablePVC countTablePV = new CountTablePVC( count.countTable(), pollName ); 361 final boolean toCorrectResults; // true iff a correction is actually needed 362 final CountNodeW specificPathNodeAtLastCount; 363 final SpecificCrosspathBarFragment specificCrosspathBarFragOrNull; 364 if( isVotingEnabled ) 365 { 366 specificPathNodeAtLastCount = countTablePV.getOrCreate( userEmail ); 367 final boolean nodeAtLastCountIsImageAndCurrent = specificPathNodeAtLastCount.isImage() 368 && specificPathNodeAtLastCount.getTime() > currentVote.getTime(); 369 final String candidateEmailOld = specificPathNodeAtLastCount.getCandidateEmail(); 370 final String candidateEmailNew = currentVote.getCandidateEmail(); 371 final AttributeAppenderS candidateLinkNewCrosspathStyler = 372 newCandidateLinkCrosspathStyler(); 373 if( nodeAtLastCountIsImageAndCurrent 374 || ObjectX.nullEquals( candidateEmailOld, candidateEmailNew )) 375 { 376 toCorrectResults = false; 377 final String candidateEmail = candidateEmailOld; // rather than new, which may be stale 378 candidateDetail = new Fragment( "candidateDetail", "candidateLyCySnFrag", 379 WP_Votespace.this ); 380 { 381 final Label label = new Label( "candidate1", 382 bunW.l( "s.wic.count.WP_Votespace.candidateLyCySn1", 383 candidateEmail == null? nobodyString: IDPair.toUsername( candidateEmail ))); 384 label.setRenderBodyOnly( true ); 385 candidateDetail.add( label ); 386 } 387 addCandidateDetail( candidateDetail, "s.wic.count.WP_Votespace.candidateLyCySn", 2, 388 candidateEmail, candidateLinkNewCrosspathStyler, nobodyString, cycle ); 389 } 390 else 391 { 392 final String vCor = getPageParameters().get( "vCor" ).toString( "y" ); 393 if( "y".equals( vCor )) toCorrectResults = true; 394 else if( "n".equals( vCor )) toCorrectResults = false; 395 else 396 { 397 VSession.get().error( "improper value for page parameter 'vCor': " + vCor ); 398 throw new RestartResponseException( new WP_Message() ); 399 } 400 401 final AttributeAppenderS candidateLinkOldCrosspathStyler = 402 newCandidateLinkCrosspathStyler(); 403 candidateLinkOldCrosspathStyler.setEnabled( !toCorrectResults ); 404 candidateLinkNewCrosspathStyler.setEnabled( toCorrectResults ); 405 406 candidateDetail = new Fragment( "candidateDetail", "candidateLyCySyFrag", 407 WP_Votespace.this ); 408 addCandidateDetail( candidateDetail, "s.wic.count.WP_Votespace.candidateLyCySy", 1, 409 candidateEmailOld, candidateLinkOldCrosspathStyler, 410 nobodyString, cycle ); 411 addCandidateDetail( candidateDetail, "s.wic.count.WP_Votespace.candidateLyCySy", 3, 412 candidateEmailNew, candidateLinkNewCrosspathStyler, 413 nobodyString, cycle ); 414 { 415 final Label label = new Label( "vCor", 416 bunW.l( "s.wic.count.WP_Votespace.vCor." + vCor )); 417 label.setRenderBodyOnly( true ); 418 candidateDetail.add( label ); 419 } 420 { 421 final PageParameters linkParameters = new PageParameters( getPageParameters() ); 422 if( toCorrectResults ) linkParameters.set( "vCor", "n" ); 423 else linkParameters.remove( "vCor" ); 424 425 final BookmarkablePageLinkX link = new BookmarkablePageLinkX( 426 "aModifier", WP_Votespace.class, linkParameters ); 427 link.setBody( bunW.l( "s.wic.count.WP_Votespace.vCorUndo." + vCor )); 428 candidateDetail.add( link ); 429 } 430 } 431 specificCrosspathBarFragOrNull = new SpecificCrosspathBarFragment(); 432 candidateDetail.add( specificCrosspathBarFragOrNull ); 433 } 434 else 435 { 436 toCorrectResults = false; 437 specificPathNodeAtLastCount = null; 438 candidateDetail = new Fragment( "candidateDetail", "candidateLnCyFrag", 439 WP_Votespace.this ); 440 specificCrosspathBarFragOrNull = null; 441 } 442 443 candidateForm.add( candidateDetail ); 444 { 445 final Fragment y = newBodyOnlyFragment( "countID", "countIDFrag", WP_Votespace.this ); 446 candidateDetail.add( y ); 447 y.add( new Label( "head", bunW.l("s.wic.count.WP_Votespace.candidateLnCy1") ) 448 .setRenderBodyOnly(true) ); 449 450 final ReadyDirectory ready = countTablePV.table().readyDirectory(); 451 final File snap = ready.snapDirectory(); 452 y.add( new ExternalLink( "a", 453 /*href*/vS.votorolaURI().toASCIIString() + "/out/vocount/" + snap.getName() + "/" 454 + ready.getName() + "/", 455 /*body*/bunA.l( "a.OutputStore.setNominalDate", 456 OutputStore.setNominalDate(new GregorianCalendar(),snap) ))); 457 458 y.add( new Label( "tail", bunW.l("s.wic.count.WP_Votespace.candidateLnCy2") ) 459 .setRenderBodyOnly(true) ); 460 } 461 462 final CountNodeW[] crosspath; 463 { 464 final CR_Vote.TracePair tP; 465 if( toCorrectResults ) // show crosspath as current path 466 { 467 tP = new CR_Vote.TracePair( poll, count, currentVote, countTablePV ); 468 if( tP.traceProjected == null ) 469 { 470 crosspath = new CountNodeW[] {}; 471 specificCrosspathNode = null; 472 } 473 else 474 { 475 crosspath = tP.traceProjected; 476 specificCrosspathNode = crosspath[0]; 477 } 478 } 479 else // show crosspath as path at last count 480 { 481 tP = null; 482 specificCrosspathNode = specificPathNodeAtLastCount; 483 if( specificCrosspathNode == null ) crosspath = new CountNodeW[] {}; 484 else crosspath = specificCrosspathNode.trace(); 485 } 486 countTablePV.setCorrecting( toCorrectResults, tP, count ); 487 } 488 489 final CountNodeW crosspathEndNode; 490 final boolean crosspathEndNodeIsCandidate; 491 if( specificCrosspathNode == null ) 492 { 493 crosspathEndNode = null; 494 crosspathEndNodeIsCandidate = false; 495 } 496 else 497 { 498 crosspathEndNode = crosspath[crosspath.length - 1]; 499 crosspathEndNodeIsCandidate = crosspathEndNode.isCandidate(); 500 } 501 502 // CASCADE MODEL 503 // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 504 // A cascade is constructed of multiple tiers, arranged along a vote path, from 505 // upstream (view left) to downstream (right). A vote path of length N nodes 506 // corresponds to a cascade of either N tiers in depth, or N + 1 if there are 507 // upstream voters off the path. 508 509 final ArrayList<Tier> tierList = new ArrayList<Tier>(); 510 final CountNodeW specificPathNode; 511 final CountNodeW[] path; 512 final CountNodeW pathEndNode; 513 final boolean pathEndNodeIsCandidate; 514 if( voterEmail().equals( NOBODY.email() )) 515 { 516 specificPathNode = null; 517 path = new CountNodeW[] {}; 518 pathEndNode = null; 519 pathEndNodeIsCandidate = false; 520 } 521 else 522 { 523 final CountNodeW origin; 524 if( voterEmail().equals( userEmail )) 525 { 526 origin = specificCrosspathNode; 527 path = crosspath; 528 } 529 else 530 { 531 origin = countTablePV.getOrCreate( voterEmail() ); 532 path = origin.trace(); 533 } 534 535 specificPathNode = origin; 536 pathEndNode = path[path.length - 1]; 537 pathEndNodeIsCandidate = pathEndNode.isCandidate(); 538 539 titleModel.setObject( titleModel.getObject() + "/" + voterUsername() ); 540 } 541 542 // base tier, root candidates and cyclers 543 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 544 { 545 final CountNodeW pathNode = pathEndNodeIsCandidate? pathEndNode: null; 546 final CountNodeW crosspathNode = crosspathEndNodeIsCandidate? crosspathEndNode: null; 547 final Tier tier = new Tier( pathNode, crosspathNode, 548 countTablePV.sublistProperBaseCandidates(), /*candidateNode*/null, count ); 549 tierList.add( tier ); 550 } 551 { 552 Tier orphanTier = null; 553 if( path.length != 0 ) 554 { 555 // upstream tiers, voters 556 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 557 if( pathEndNodeIsCandidate ) 558 { 559 for( int p = path.length - 1; p >= 0; --p ) 560 { 561 final CountNodeW candidateNode = path[p]; 562 final CountNodeW pathNode; // of voter 563 if( p == 0 ) 564 { 565 if( !candidateNode.isCandidate() ) break; // top-most node has no voters 566 567 pathNode = null; 568 } 569 else pathNode = path[p - 1]; 570 final int q = p + crosspath.length - path.length; 571 final CountNodeW crosspathNode; 572 if( q > 0 && crosspath[q].equals( candidateNode )) // if share same candidate 573 { 574 crosspathNode = crosspath[q - 1]; 575 } 576 else crosspathNode = null; 577 final Tier tier = new Tier( pathNode, crosspathNode, 578 countTablePV.sublistProperCasters(candidateNode.email()), candidateNode, 579 count ); 580 assert toCorrectResults || tier.properNodesList.size() > 0; /* vote 581 path length or candidacy implies voters, unless correcting and 582 voter (not really counted yet) lacks dart sector */ 583 tierList.add( 0, tier ); 584 } 585 } 586 else // single non-voter/non-candidate orphan 587 { 588 assert path.length == 1; // non-candidate node in base tier implies path length 1 589 final Tier tier = new Tier( /*pathNode*/pathEndNode, pathEndNode ); 590 tierList.add( 0, tier ); 591 orphanTier = tier; 592 } 593 } 594 } 595 final int tN = tierList.size(); 596 for( int t = 0; t < tN; ++t ) tierList.get(t).initPlace( specificPathNode, t, tN ); 597 598 // CASCADE VIEW 599 // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 600 // The view runs horizontally from the leftmost tier (upstream) to the rightmost 601 // (downstream). Voters/candidates are stacked vertically in each tier. The layout 602 // is a "cascading table". It is similar to a "cascading list", except that each 603 // stacked node has multiple vertical columns for its various properties, such as 604 // vote counts, email address, and links. 605 606 int maxRowCount = 0; 607 for( int t = 0; t < tN; ++t ) 608 { 609 final Tier tier = tierList.get( t ); 610 final int rowCount = tier.rowCount(); 611 if( rowCount > maxRowCount ) maxRowCount = rowCount; 612 } 613 { 614 final RepeatingView tierRepeatingCol = new RepeatingView( "tierRepeatCol" ); 615 yCount.add( tierRepeatingCol ); 616 final RepeatingView tierRepeatingHead = new RepeatingView( "tierRepeatHead" ); 617 yCount.add( tierRepeatingHead ); 618 for( int t = 0, tLast = tN - 1; t <= tLast; ++t ) // left to right 619 { 620 // Columns row 621 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 622 tierRepeatingCol.add( newBodyOnlyFragment( tierRepeatingCol.newChildId(), 623 "colRowFrag", WP_Votespace.this )); 624 625 // Header row 626 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 627 final CountNodeW candidateNode; 628 if( t != tLast ) 629 { 630 final Tier candidateTier = tierList.get( t + 1 ); 631 candidateNode = candidateTier.pathNode; 632 } 633 else candidateNode = null; 634 635 final Fragment y = newBodyOnlyFragment( tierRepeatingHead.newChildId(), 636 "headerRowFrag", WP_Votespace.this ); 637 tierRepeatingHead.add( y ); 638 639 // receive volume 640 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 641 { 642 final Label th = new Label( "receiveVolume", 643 bunW.l( "s.wic.count.WP_Votespace.th.receiveCount.short" )); 644 th.add( AttributeModifier.replace( "title", 645 bunW.l( "s.wic.count.WP_Votespace.th.receiveCount" ))); 646 y.add( th ); 647 } 648 649 // user mnemonic + label 650 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 651 { 652 final String key; 653 if( t == tLast ) key = "s.wic.count.WP_Votespace.th.voterEmail.end"; 654 else if( candidateNode == null ) 655 { 656 key = "s.wic.count.WP_Votespace.th.voterEmail.orphan"; 657 } 658 else key = "s.wic.count.WP_Votespace.th.voterEmail"; 659 y.add( new Label( "voterEmail", bunW.l( key ))); 660 } 661 662 // hold volume 663 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 664 { 665 final WebMarkupContainer th = new WebMarkupContainer( "holdVolume" ); 666 y.add( th ); 667 th.add( AttributeModifier.replace( "title", 668 bunW.l( "s.wic.count.WP_Votespace.th.holdCount" ))); 669 th.add( new Label( "span", 670 bunW.l( "s.wic.count.WP_Votespace.th.holdCount.short" ))); 671 } 672 673 // cast volume 674 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 675 { 676 final WebMarkupContainer th = new WebMarkupContainer( "castVolume" ); 677 y.add( th ); 678 th.add( AttributeModifier.replace( "title", 679 bunW.l( "s.wic.count.WP_Votespace.th.singleCastCount" ))); 680 th.add( new Label( "span", 681 bunW.l( "s.wic.count.WP_Votespace.th.singleCastCount.short" ))); 682 } 683 684 // outflow volume 685 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 686 { 687 final WebMarkupContainer th = new WebMarkupContainer( "outflowVolume" ); 688 y.add( th ); 689 th.add( AttributeModifier.replace( "title", 690 bunW.l( "s.wic.count.WP_Votespace.th.castCarryCount" ))); 691 th.add( new Label( "span", 692 bunW.l( "s.wic.count.WP_Votespace.th.castCarryCount.short" ))); 693 } 694 } 695 } 696 final RepeatingView rowRepeating = new RepeatingView( "repeat" ); 697 yCount.add( rowRepeating ); 698 final SimpleDateFormat iso8601Formatter = 699 new SimpleDateFormat( SimpleDateFormatX.ISO_8601_PATTERN ); 700 final Date date = new Date( 0L ); 701 final StringBuilder b = new StringBuilder(); 702 for( int r = 0; r < maxRowCount; ++r ) // data rows, top to bottom 703 { 704 final WebMarkupContainer row = new WebMarkupContainer( rowRepeating.newChildId() ); 705 rowRepeating.add( row ); 706 final RepeatingView tierRepeating = new RepeatingView( "tierRepeat" ); 707 row.add( tierRepeating ); 708 for( int t = 0, tLast = tN - 1; t <= tLast; ++t ) // left to right 709 { 710 final Tier tier = tierList.get( t ); 711 final int lastNodeRow = tier.lastNodeRow(); 712 final Tier candidateTier; 713 final CountNodeW candidateNode; 714 if( t != tLast ) 715 { 716 candidateTier = tierList.get( t + 1 ); 717 candidateNode = candidateTier.pathNode; 718 } 719 else 720 { 721 candidateTier = null; 722 candidateNode = null; 723 } 724 final Fragment y; 725 726 // Footnote Row 727 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 728 if( r >= tier.footnoteRow ) 729 { 730 y = newBodyOnlyFragment( tierRepeating.newChildId(), "footnoteRowFrag", 731 WP_Votespace.this ); 732 733 // footnote 734 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 735 if( r == tier.footnoteRow ) 736 { 737 final WebMarkupContainer td = new WebMarkupContainer( "footnote" ); 738 td.add( AttributeModifier.replace( "rowspan", 739 Integer.toString( maxRowCount - r ))); 740 tier.footnoteBuilder.td = td; 741 y.add( td ); 742 743 if( t < tLast && candidateNode == null ) appendStyleClass( td, "orphan" ); 744 } 745 else y.add( newNullComponent( "footnote" )); 746 747 // outflow image 748 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 749 addOutflowImage( r, y, tier, candidateTier, cycle ); 750 } 751 752 // Sum row 753 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 754 else if( tier.sumRow != -1 && r > tier.sumRow ) // it must be the 2nd sum row 755 { 756 y = newBodyOnlyFragment( tierRepeating.newChildId(), "sumRow2Frag", 757 WP_Votespace.this ); 758 759 // outflow image 760 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 761 addOutflowImage( r, y, tier, candidateTier, cycle ); 762 } 763 else if( r == tier.sumRow ) 764 { 765 // hold volume 766 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 767 if( t == tLast ) // base tier 768 { 769 y = newBodyOnlyFragment( tierRepeating.newChildId(), "sumHRowFrag", 770 WP_Votespace.this ); 771 772 y.add( new Label( "turnout", 773 bunW.l( "s.wic.count.WP_Votespace.turnout" )).setRenderBodyOnly( true )); 774 775 final Fragment sup = new Fragment( "sup", "footnoteCallFrag", 776 WP_Votespace.this ); 777 final long nTurnout = count.holdVolume(); 778 final long nEligible = poll.populationSize(); 779 final String footnoteBody; 780 if( nEligible > 0 ) 781 { 782 footnoteBody = bunW.l( 783 "s.wic.count.WP_Votespace.turnout_XHT", 784 nTurnout, nEligible, nTurnout * 100d / nEligible ); 785 } 786 else footnoteBody = bunW.l( "s.wic.count.WP_Votespace.turnout0_XHT" ); 787 // turnout cannot be calculated 788 final Footnote footnote = new Footnote( footnoteBody ); 789 sup.add( footnote.newCallLink() ); 790 tier.footnoteBuilder.append( footnote ); 791 y.add( sup ); 792 y.add( new Label( "holdVolume", bunA.format( "%,d", nTurnout ))); 793 } 794 795 // outflow volume 796 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 797 else // voter tier 798 { 799 y = newBodyOnlyFragment( tierRepeating.newChildId(), "sumCCRowFrag", 800 WP_Votespace.this ); 801 y.add( new Label( "outflowVolume", 802 bunA.format( "%,d", candidateNode.receiveVolume() ))); 803 } 804 805 // outflow image 806 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 807 addOutflowImage( r, y, tier, candidateTier, cycle ); 808 } 809 810 // Other row 811 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 812 else if( r == tier.otherNodesRow ) 813 { 814 y = newBodyOnlyFragment( tierRepeating.newChildId(), "otherNodesRowFrag", 815 WP_Votespace.this ); 816 817 // user mnemonic + label 818 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 819 final long holdVolume = tier.otherNodesCumulate.holdVolume; 820 y.add( new Label( "other", bunW.l( 821 t == tLast && holdVolume == Tier.CountCumulate.NO_PARTICIPANTS? 822 "s.wic.count.WP_Votespace.otherNodes0": 823 "s.wic.count.WP_Votespace.otherNodes" ))); 824 825 // hold count 826 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 827 y.add( new Label( "holdVolume", holdVolume == -1? 828 "": // non-base tier, cumulative data not calculated 829 bunA.format( "%,d", holdVolume ))); 830 831 // outflow volume 832 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 833 final long outflowVolume = tier.otherNodesCumulate.outflowVolume; 834 y.add( new Label( "outflowVolume", outflowVolume == -1? 835 "": // base tier, cumulative data not calculated 836 bunA.format( "%,d", outflowVolume ))); 837 838 // outflow image 839 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 840 addOutflowImage( r, y, tier, candidateTier, cycle, /*node*/null ); 841 } 842 843 // Node row 844 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 845 else 846 { 847 y = newBodyOnlyFragment( tierRepeating.newChildId(), "nodeRowFrag", 848 WP_Votespace.this ); 849 850 final CountNodeW node = tier.getNode( r ); 851 final String nodeUsername = node.person().username(); 852 final boolean isPathNode; 853 final boolean isSpecificPathNode; 854 if( r == tier.pathRow ) 855 { 856 isPathNode = true; 857 isSpecificPathNode = node.equals( specificPathNode ); 858 } 859 else 860 { 861 isPathNode = false; 862 isSpecificPathNode = false; 863 } 864 865 // inflow 866 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 867 { 868 final WebMarkupContainer td = new WebMarkupContainer( "inflow" ); 869 y.add( td ); 870 871 final Component img; 872 if( isPathNode && node.isCandidate() ) 873 { 874 appendStyleClass( td, "f" ); // flow 875 appendStyleClass( td, "i" ); // in 876 877 img = new WebMarkupContainer( "img" ); 878 final String imgName = isSpecificPathNode? "f-i": "f-i-path"; 879 img.add( AttributeModifier.replace( "src", 880 cycle.vRequest().getContextPath() 881 + "/count/WP_Votespace/" + imgName + ".png" )); 882 } 883 else img = newNullComponent( "img" ); 884 td.add( img ); 885 } 886 887 // receive volume 888 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 889 final Label rTD = new Label( "receiveVolume", 890 bunA.format("%,d",node.receiveVolume()) ); 891 y.add( rTD ); 892 if( isPathNode ) appendStyleClass( rTD, "dpath" ); 893 894 // user mnemonic 895 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 896 final Label voterEmailSpan; 897 { 898 final WebMarkupContainer td = new WebMarkupContainer( "voterEmail" ); 899 y.add( td ); 900 if( r == tier.crosspathRow ) appendStyleClass( td, "crosspath" ); 901 voterEmailSpan = new Label( "span", 902 IDPair.buildUserMnemonic(nodeUsername,StringBuilderX.clear(b)) 903 .toString() ); 904 td.add( voterEmailSpan ); 905 } 906 907 // display title 908 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 909 final long castVolume = node.castVolume(); 910 final long outflowVolume = castVolume + node.carryVolume(); 911 { 912 final boolean isOrphan = t < tLast && !node.isVoter(); 913 final boolean toEnable = !isOrphan; 914 // otherwise click on node and it vanishes (too surprising) 915 final PageParameters linkParameters = 916 new PageParameters( getPageParameters() ); 917 linkParameters.remove( "v" ); 918 if( isSpecificPathNode ) // leftmost on path 919 { 920 if( path.length == 1 ) linkParameters.remove( "u" ); 921 // clear single-node path to no path at all 922 else // else unroll the path, rightward 923 { 924 linkParameters.set( "u", IDPair.toUsername( 925 node.getCandidateEmail() )); 926 } 927 } 928 else linkParameters.set( "u", nodeUsername ); // make it the specific node 929 930 final WebMarkupContainer td; 931 { 932 if( toEnable ) 933 { 934 td = new BookmarkablePageLinkX( // JavaScript link 935 "displayTitle", WP_Votespace.class, linkParameters ); 936 appendStyleClass( td, "k" ); // clickable 937 if( tier.place != XCastRelation.UNKNOWN ) 938 { 939 td.add( new AttributeModifier( "id", tier.place.symbol() + 940 Byte.toString(node.dartSector()) )); 941 td.add( new AttributeModifier( "onmouseover", 942 "_s_wic_count_WP_Votespace.dartSpotOn(this)" )); 943 td.add( new AttributeModifier( "onmouseout", 944 "_s_wic_count_WP_Votespace.dartSpotOff(this)" )); 945 } 946 } 947 else td = new WebMarkupContainer( "displayTitle" ); 948 y.add( td ); 949 } 950 if( isPathNode ) appendStyleClass( td, "dpath" ); 951 final BookmarkablePageLinkX link = new BookmarkablePageLinkX( // ordinary link nested in JavaScript link 952 "a", WP_Votespace.class, linkParameters ); 953 final String displayTitle = node.displayTitle(); 954 final String linkBody; 955 if( displayTitle == null ) linkBody = nodeUsername; 956 else 957 { 958 linkBody = displayTitle; 959 appendStyleClass( td, "dt" ); 960 appendStyleClass( rTD, "dt" ); 961 voterEmailSpan.add( AttributeModifier.replace( "title", nodeUsername )); 962 } 963 link.setBody( linkBody ); 964 link.setEnabled( toEnable ); 965 td.add( link ); 966 if( isOrphan ) 967 { 968 final Footnote footnote = new Footnote( bunW.l( 969 "s.wic.count.WP_Votespace.orphanVoter-non_XHT" )); 970 tier.footnoteBuilder.append( footnote ); 971 final Fragment sup = new Fragment( "sup", "footnoteCallFrag", 972 WP_Votespace.this ); 973 sup.add( footnote.newCallLink() ); 974 td.add( sup ); 975 } 976 else if( isVoterAndBarred( node )) 977 { 978 final Footnote footnote; 979 if( node.equals( specificCrosspathNode )) 980 { 981 specificCrosspathBarFragOrNull.init( tier.footnoteBuilder, cycle ); 982 footnote = specificCrosspathBarFragOrNull.footnote; 983 if( footnote == null ) throw new NullPointerException(); // fail fast 984 } 985 else 986 { 987 footnote = new Footnote( "<p>" 988 + bunA.l( "a.count.voteBar", nodeUsername, 989 IDPair.toUsername( node.getCandidateEmail() ), node.getBar() ) 990 + "</p>" ); 991 tier.footnoteBuilder.append( footnote ); 992 } 993 final Fragment sup = new Fragment( "sup", "footnoteCallFrag", 994 WP_Votespace.this ); 995 sup.add( footnote.newCallLink() ); 996 td.add( sup ); 997 } 998 else td.add( newNullComponent( "sup" )); 999 } 1000 1001 // hold volume 1002 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 1003 { 1004 final Label td = new Label( "holdVolume", 1005 bunA.format("%,d",node.holdVolume()) ); 1006 y.add( td ); 1007 if( isPathNode ) appendStyleClass( td, "dpath" ); 1008 } 1009 1010 // cast volume 1011 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 1012 { 1013 final Model<String> href = new Model<String>(); 1014 final ExternalLink a = new ExternalLink( "castVolume", href, 1015 /*label*/new Model<String>(bunA.format("%,d",castVolume)) ); 1016 y.add( a ); 1017 if( castVolume == 0L ) a.setEnabled( false ); // no vote 1018 else 1019 { 1020 final String source = node.getSource(); 1021 if( source == null ) 1022 { 1023 href.setObject( vS.votorolaURI().toASCIIString() 1024 + "/out/vocount/_snap_report/_in_vote/" + pollName + ".xml" ); 1025 } 1026 else // vote is mirror image 1027 { 1028 href.setObject( vS.votorolaURI().toASCIIString() + "/in/vomir/" 1029 + source + "/_snap_current/" + pollName + ".xml" ); 1030 appendStyleClass( a, "mir" ); 1031 } 1032 } 1033 } 1034 1035 // outflow volume 1036 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 1037 { 1038 final Label td = new Label( "outflowVolume", 1039 bunA.format("%,d",outflowVolume) ); 1040 y.add( td ); 1041 if( isPathNode ) appendStyleClass( td, "dpath" ); 1042 } 1043 1044 // outflow image 1045 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 1046 addOutflowImage( r, y, tier, candidateTier, cycle, node ); 1047 } 1048 tierRepeating.add( y ); 1049 } 1050 1051 // startCol and endCol (first row only) 1052 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 1053 if( r == 0 ) 1054 { 1055 final AttributeModifier sAM = 1056 AttributeModifier.replace( "rowspan", Integer.toString( maxRowCount )); 1057 { 1058 final WebMarkupContainer td = new WebMarkupContainer( "startCol" ); 1059 td.add( sAM ); 1060 row.add( td ); 1061 } 1062 { 1063 final WebMarkupContainer td = new WebMarkupContainer( "endCol" ); 1064 td.add( sAM ); 1065 row.add( td ); 1066 } 1067 } 1068 else 1069 { 1070 row.add( newNullComponent( "startCol" )); 1071 row.add( newNullComponent( "endCol" )); 1072 } 1073 } 1074 1075 // FOOTNOTES 1076 // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 1077 if( specificCrosspathBarFragOrNull != null && 1078 !specificCrosspathBarFragOrNull.initWasCalled ) // i.e. footnote not placed in orphan tier 1079 { 1080 specificCrosspathBarFragOrNull.init( tierList.get(tN-1).footnoteBuilder, cycle ); 1081 // place in final tier 1082 } 1083 for( int f = 0, t = 0; t < tN; ++t ) 1084 { 1085 final Tier tier = tierList.get( t ); 1086 final FootnoteBuilder fB = tier.footnoteBuilder; 1087 final MarkupContainer container; 1088 if( fB.list.size() == 0 ) container = newNullComponent( "container" ); 1089 else 1090 { 1091 appendStyleClass( fB.td, "footnote" ); 1092 container = new Fragment( "container", "footnoteTableFrag", WP_Votespace.this ); 1093 final RepeatingView repeating = new RepeatingView( "repeat" ); 1094 container.add( repeating ); 1095 1096 final int fTN = fB.list.size(); 1097 for( int fT = 0;; ) 1098 { 1099 final Footnote footnote = fB.list.get( fT ); 1100 final WebMarkupContainer y = new WebMarkupContainer( repeating.newChildId() ); 1101 repeating.add( y ); 1102 ++f; 1103 // final String fString = Integer.toString( f ); 1104 /// but letters are better in a numeric display, so try this hack: 1105 final String fString = Integer.toString( f + 9, /*radix*/36 ); 1106 // final String noteLinkHref = "#fc-" + fString; 1107 /// (a) it's not helpful, so defeat actual linking: 1108 final String noteLinkHref = null; 1109 final ExternalLink noteLink = new ExternalLink( "link", noteLinkHref, fString ); 1110 noteLink.add( AttributeModifier.replace( "id", "fn-" + fString )); 1111 y.add( noteLink ); 1112 for( int c = footnote.callLinkList.size() - 1;; --c ) 1113 { 1114 final ExternalLink callLink = footnote.callLinkList.get( c ); 1115 // callLink.setDefaultModelObject( "#fn-" + fString ); // defeated per (a) above 1116 IModelX.setObject( callLink.getBody(), fString ); 1117 if( c == 0 ) 1118 { 1119 callLink.add( AttributeModifier.replace( "id", "fc-" + fString )); 1120 // only the first call gets this 1121 break; 1122 } 1123 } 1124 y.add( new Label( "body", footnote.body ).setEscapeModelStrings( false )); 1125 ++fT; 1126 if( fT >= fTN ) 1127 { 1128 appendStyleClass( y, "last" ); 1129 break; 1130 } 1131 } 1132 } 1133 fB.td.add( container ); 1134 } 1135 } 1136 1137 1138 1139 // ------------------------------------------------------------------------------------ 1140 1141 1142 /** Effects a recall-redirect if requested. Any parameters named by a 1143 * 'recallRedirect' parameter are recalled and a 303 redirect exception is thrown. 1144 */ 1145 static void maybeRecallRedirect( final Class<? extends Page> pageClass, final PageParameters pP, 1146 final VRequestCycle cycle ) 1147 { 1148 // final String value = (String)p.remove( "recallRedirect" ); 1149 /// but Wicket 1.3 is storing parameter values as arrays, and not documenting it, so this is easier: 1150 final String value = pP.get( "recallRedirect" ).toString(); 1151 if( value == null ) return; 1152 1153 pP.remove( "recallRedirect" ); 1154 final Set<String> pOld = pP.getNamedKeys(); 1155 final Set<String> pNew = PageParametersX.splitAsSet( value, 1156 PageParametersX.SPLIT_ON_BAR_PATTERN ); 1157 if( pNew.contains( "p" ) && !pOld.contains( "p" )) WP_Poll.withRecall_p( pP ); 1158 if( (pNew.contains( "u" ) || pNew.contains( "v" )) 1159 && !pOld.contains( "u" ) && !pOld.contains( "v" )) VoterPage.U.withRecall_u_v( pP ); 1160 throw new RedirectException( cycle.uriFor(pageClass,pP).toASCIIString(), 303 ); 1161 } 1162 1163 1164 1165 /** Returns the query parameters for a particular votespace page. The voter is set as 1166 * the last fore-navigated to. 1167 * 1168 * @see votorola.a.voter.VoterPage.SessionScope#getLastIDPair() 1169 */ 1170 public static PageParameters parameters( final String serviceName, final VRequestCycle cycle ) 1171 { 1172 final PageParameters pP = new PageParameters(); 1173 pP.set( "p", Poll.U.toQuery( serviceName )); 1174 return VoterPage.U.withRecall_u_v( pP ); 1175 } 1176 1177 1178 1179 public @Warning("non-API") IDPair getNewCandidate() { return newCandidate; }; 1180 1181 1182 private IDPair newCandidate; 1183 1184 1185 public @Warning("non-API") void setNewCandidate( IDPair _newCandidate ) // public for sake of Wicket property models only 1186 { 1187 newCandidate = _newCandidate; 1188 newVote.setCandidateEmail( NOBODY.equals(_newCandidate)? null: _newCandidate.email() ); 1189 }; 1190 1191 1192 1193 // - T a b b e d - P a g e ------------------------------------------------------------ 1194 1195 1196 /** @see #NAV_TAB 1197 */ 1198 public NavTab navTab( VRequestCycle cycle ) { return NAV_TAB; } 1199 1200 1201 1202 /** The navigation tab that fetches the votespace page, an instance of WP_Votespace. 1203 */ 1204 public static final NavTab NAV_TAB = new VotespaceTab( WP_Votespace.class ) 1205 { 1206 public @Override String shortTitle( final VRequestCycle cycle ) 1207 { 1208 return cycle.bunW().l( "s.wic.count.WP_Votespace.tab.shortTitle" ); 1209 } 1210 }; 1211 1212 1213 1214 // - V o t e r - P a g e -------------------------------------------------------------- 1215 1216 1217 public String voterEmail() { return voterIDPair.email(); } 1218 1219 1220 1221 public IDPair voterIDPair() { return voterIDPair; } 1222 1223 1224 private final IDPair voterIDPair; 1225 1226 1227 1228 public String voterUsername() { return voterIDPair.username(); } 1229 1230 1231 1232 // ==================================================================================== 1233 1234 1235 /** An aid for constructing a set of footnotes. 1236 */ 1237 static final class FootnoteBuilder 1238 { 1239 1240 1241 /** Appends a footnote to the set. 1242 */ 1243 void append( final Footnote footnote ) 1244 { 1245 if( list.size() == 0 ) list = new ArrayList<Footnote>( /*init capacity*/4 ); 1246 1247 list.add( footnote ); 1248 } 1249 1250 1251 /** The read-only list of footnotes. To append footnotes to the list, use 1252 * append(). 1253 */ 1254 List<Footnote> list = Collections.emptyList(); 1255 1256 1257 /** The footnote cell at the bottom of the tier, or null if the footnotes are to 1258 * be placed outside of any tier. A single set of footnotes is placed outside of 1259 * a tier when there is no count. 1260 */ 1261 WebMarkupContainer td; 1262 1263 } 1264 1265 1266 1267 // ==================================================================================== 1268 1269 1270 static abstract @ThreadSafe class VotespaceTab extends NavTab 1271 { 1272 1273 /** Contructs a VotespaceTab. 1274 */ 1275 VotespaceTab( Class<? extends Page> _pageClass ) { pageClass = _pageClass; } 1276 1277 1278 // - N a v - T a b ---------------------------------------------------------------- 1279 1280 1281 public @Override final Bookmark bookmark() 1282 { 1283 PageParameters pP = null; 1284 pP = WP_Poll.withRecall_p( pP ); 1285 pP = VoterPage.U.withRecall_u_v( pP ); 1286 return new Bookmark( pageClass, pP ); 1287 } 1288 1289 1290 public @Override Class<? extends Page> pageClass() { return pageClass; } 1291 1292 1293 private final Class<? extends Page> pageClass; 1294 1295 1296 } 1297 1298 1299 1300//// P r i v a t e /////////////////////////////////////////////////////////////////////// 1301 1302 1303 private void addCandidateDetail( final MarkupContainer y, final String baseKey, int suffix, 1304 final String email, final AttributeAppender styler, final String nobodyString, 1305 final VRequestCycle cycle ) 1306 { 1307 y.add( newCandidateDetailLabel( baseKey, suffix, cycle )); 1308 y.add( newCandidateDetailVLink( suffix, email, nobodyString ).add( styler )); 1309 ++suffix; 1310 y.add( newCandidateDetailLabel( baseKey, suffix, cycle )); 1311 } 1312 1313 1314 1315 /** Adds outflow for a non-node row. 1316 */ 1317 private void addOutflowImage( final int r, final WebMarkupContainer y, final Tier tier, 1318 final Tier candidateTier, final VRequestCycle cycle ) 1319 { 1320 final WebMarkupContainer td = new WebMarkupContainer( "outflow" ); 1321 y.add( td ); 1322 Component img = null; // so far 1323 if( candidateTier != null ) 1324 { 1325 int rCandidate = candidateTier.pathRow; 1326 if( r <= rCandidate ) 1327 { 1328 appendStyleClass( td, "f" ); // flow 1329 appendStyleClass( td, "o" ); // out 1330 if( tier.pathNode != null ) appendStyleClass( td, "path" ); 1331 1332 if( r == rCandidate ) 1333 { 1334 img = new WebMarkupContainer( "img" ); 1335 final StringBuilder b = new StringBuilder(); 1336 b.append( cycle.vRequest().getContextPath() ); 1337 b.append( "/count/WP_Votespace/f-DR-clear-bottom" ); 1338 1339 if( tier.pathNode != null && 1340 ( r == tier.pathRow || r == rCandidate )) b.append( "-path" ); 1341 b.append( ".png" ); 1342 img.add( AttributeModifier.replace( "src", b.toString() )); 1343 } 1344 // else empty cell, showing only the background image 1345 } 1346 } 1347 if( img == null ) img = newNullComponent( "img" ); 1348 td.add( img ); 1349 } 1350 1351 1352 1353 /** Adds outflow for a node row (proper, other, or external path). 1354 * 1355 * @param node the count node, or null in the case of an "other" row. 1356 */ 1357 private void addOutflowImage( final int r, final WebMarkupContainer y, final Tier tier, 1358 final Tier candidateTier, final VRequestCycle cycle, final CountNodeW node ) 1359 { 1360 final WebMarkupContainer td = new WebMarkupContainer( "outflow" ); 1361 y.add( td ); 1362 final MarkupContainer img; 1363 if( candidateTier == null ) img = newNullComponent( "img" ); 1364 else if( node == null/*other row*/ || node.isVoter() ) 1365 { 1366 int rCandidate = candidateTier.pathRow; 1367 appendStyleClass( td, "f" ); // flow 1368 appendStyleClass( td, "o" ); // out 1369 if( tier.pathNode != null ) 1370 { 1371 if( r > tier.pathRow && r <= rCandidate 1372 || r <= tier.pathRow && r > rCandidate ) appendStyleClass( td, "path" ); 1373 } 1374 img = new WebMarkupContainer( "img" ); 1375 final int lastNodeRow = tier.lastNodeRow(); 1376 final StringBuilder b = new StringBuilder(); 1377 b.append( cycle.vRequest().getContextPath() ); 1378 b.append( "/count/WP_Votespace/f-" ); 1379 if( r == 0 ) 1380 { 1381 appendStyleClass( td, "top" ); 1382 if( r == rCandidate ) 1383 { 1384 if( r == lastNodeRow ) b.append( "R-single" ); 1385 else if( tier.pathNode == null ) b.append( "UR-top" ); // covers everything in this case, and so there are no other images 1386 else if( r == tier.pathRow ) b.append( "R-top" ); 1387 else b.append( "UR-top" ); 1388 } 1389 else b.append( "RD-top" ); 1390 } 1391 else if( r < rCandidate ) 1392 { 1393 if( r == tier.pathRow ) b.append( "RD-top" ); 1394 else b.append( "RD" ); // special case 1395 } 1396 else if( r == lastNodeRow ) 1397 { 1398 if( r == rCandidate ) b.append( "DR-bottom" ); 1399 else b.append( "RU-bottom" ); 1400 } 1401 else 1402 { 1403 if( r == rCandidate ) 1404 { 1405 if( tier.pathNode == null ) b.append( "R" ); // covers everything in this case, and so there are no other images 1406 else if( r > tier.pathRow ) b.append( "DR" ); 1407 else if( r < tier.pathRow ) b.append( "UR" ); 1408 else b.append( 'R' ); 1409 } 1410 else b.append( "RU" ); 1411 } 1412 if( tier.pathNode != null && 1413 ( r == tier.pathRow || r == rCandidate )) b.append( "-path" ); 1414 b.append( ".png" ); 1415 img.add( AttributeModifier.replace( "src", b.toString() )); 1416 } 1417 else // non-voter 1418 { 1419 appendStyleClass( td, "f" ); // flow 1420 appendStyleClass( td, "o" ); // out 1421 appendStyleClass( td, "non" ); 1422 img = new WebMarkupContainer( "img" ); 1423 img.add( AttributeModifier.replace( "src", cycle.vRequest().getContextPath() 1424 + "/count/WP_Votespace/f-non.png" )); 1425 } 1426 td.add( img ); 1427 } 1428 1429 1430 1431 /** @see #newVote 1432 */ 1433 private Vote currentVote; // final after init 1434 1435 1436 1437 /** Return true iff the node is both voting and barred. The presence of a bar is not 1438 * enough to test for this condition, because non-voting nodes are not checked for 1439 * bars during the count (an optimization), but instead are left with the pseudo-bar 1440 * "voterBarUnknown". Hence this added complexity. 1441 */ 1442 private static boolean isVoterAndBarred( final CountNodeW node ) 1443 { 1444 return node.isCast() && node.getBar() != null; 1445 } 1446 1447 1448 1449 private Label newCandidateDetailLabel( final String baseKey, final int suffix, 1450 final VRequestCycle cycle ) 1451 { 1452 final Label label = new Label( "candidate" + suffix, cycle.bunW().l( baseKey + suffix )); 1453 label.setRenderBodyOnly( true ); 1454 return label; 1455 } 1456 1457 1458 1459 private BookmarkablePageLinkX newCandidateDetailVLink( final int suffix, final String email, 1460 final String nobodyString ) 1461 { 1462 final PageParameters linkParameters = new PageParameters( getPageParameters() ); 1463 linkParameters.remove( "v" ); 1464 boolean toEnableLink = false; // so far 1465 final String username; 1466 if( email == null ) 1467 { 1468 username = nobodyString; 1469 linkParameters.remove( "u" ); 1470 toEnableLink = !voterEmail().equals( NOBODY.email() ); 1471 } 1472 else if( email.equals( voterEmail() )) username = voterUsername(); 1473 else 1474 { 1475 username = IDPair.toUsername( email ); 1476 linkParameters.set( "u", username ); 1477 toEnableLink = true; 1478 } 1479 1480 final BookmarkablePageLinkX link = 1481 new BookmarkablePageLinkX( "a" + suffix, WP_Votespace.class, linkParameters ); 1482 link.setBody( username ); 1483 link.setEnabled( toEnableLink ); 1484 return link; 1485 } 1486 1487 1488 1489 private static AttributeAppenderS newCandidateLinkCrosspathStyler() 1490 { 1491 return new AttributeAppenderS( "class", new Model<String>("crosspath"), " " ); 1492 } 1493 1494 1495 1496 /** @see #currentVote 1497 */ 1498 private Vote newVote; // final after init, don't set candidate directly, but use setNewCandidate 1499 1500 1501 1502 private static final String[] pKey = { "p", "recallRedirect", "u", "v", "vCor" }; 1503 // known parameters 1504 1505 1506 1507 private final String pollName; 1508 1509 1510 1511 private CountNodeW specificCrosspathNode; // or null, final after init 1512 1513 1514 1515 // ==================================================================================== 1516 1517 1518 private final class CandidateForm extends StatelessForm<Void> 1519 { 1520 1521 private CandidateForm() { super( "candidate" ); } 1522 1523 1524 protected @Override void onSubmit() 1525 { 1526 super.onSubmit(); 1527 // if( !isVotingEnabled ) throw new IllegalStateException(); // probably impossible 1528 //// no need, access is guarded in VoterInputTable 1529 1530 final VRequestCycle cycle = VRequestCycle.get(); 1531 final Component submitter = (Component)findSubmittingButton(); 1532 1533 // Go 1534 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1535 if( submitter == null || "go".equals( submitter.getId() )) 1536 { 1537 setResponsePage( VRequestCycle.get() ); 1538 return; 1539 } 1540 1541 // Vote or Unvote 1542 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1543 if( VSession.get().user() == null ) throw new PageExpiredException( /*message*/null ); 1544 // fail fast, user has logged out and navigated back to this page 1545 1546 if( "unvote".equals( submitter.getId() )) setNewCandidate( NOBODY ); // and thence newVote 1547 else assert "vote".equals( submitter.getId() ); 1548 try 1549 { 1550 final PollService poll = WP_Poll.pollFor( pollName, cycle ); 1551 newVote.write( poll.voterInputTable(), VSession.get(), /*toForce*/true ); 1552 VOWicket.get().scopeActivity().activityList().log( 1553 poll.newChangeEventOrNull( currentVote, newVote )); 1554 setResponsePage( cycle ); 1555 } 1556 catch( Exception x ) { throw VotorolaRuntimeException.castOrWrapped( x ); } 1557 } 1558 1559 1560 private void setResponsePage( final VRequestCycle cycle ) 1561 { 1562 final PageParameters oldP = getPageParameters(); 1563 final PageParameters newP = new PageParameters(); 1564 for( final String key: pKey ) // strip stateless form's submission parameters 1565 { 1566 final List<StringValue> values = oldP.getValues( key ); // though there's only ever the one in this case 1567 for( final StringValue v: values ) newP.add( key, v ); 1568 } 1569 VoterPage.U.setFrom( newCandidate, newP ); 1570 cycle.setResponsePage( WP_Votespace.class, newP ); 1571 } 1572 1573 } 1574 1575 1576 1577 // ==================================================================================== 1578 1579 1580 private static @ThreadRestricted final class CorrectableCount extends Count 1581 { 1582 1583 CorrectableCount( final Count count ) { super( count ); } 1584 1585 1586 private long castCorrection; 1587 1588 1589 // - C o u n t -------------------------------------------------------------------- 1590 1591 1592 public long castVolume() { return super.castVolume() + castCorrection; } 1593 1594 } 1595 1596 1597 1598 // ==================================================================================== 1599 1600 1601 /** A cached view of a count table restricted to a particular poll. 1602 */ 1603 private static @ThreadRestricted final class CountTablePVC extends CR_Vote.CountTablePVC 1604 { 1605 1606 CountTablePVC( CountTable t, String serviceName ) { super( t, serviceName ); } 1607 1608 1609 private void correct( final ArrayList<CountNodeW> nodeList, 1610 final QueryConstraintTester tester ) 1611 { 1612 // Substitute changed nodes, remove any that no longer meet query contraints 1613 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1614 { 1615 final ListIterator<CountNodeW> nodeListI = nodeList.listIterator(); 1616 while( nodeListI.hasNext() ) 1617 { 1618 final CountNodeW node = nodeListI.next(); 1619 final CountNodeW correctedNode = cache.get( node.email() ); 1620 if( correctedNode == null ) continue; // node unaffected by vote shift 1621 1622 if( tester.meetsConstraints( correctedNode )) nodeListI.set( correctedNode ); 1623 else nodeListI.remove(); 1624 } 1625 } 1626 1627 // Add changed nodes that now meet query contraints 1628 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1629 for( final CountNodeW correctedNode: cache.values() ) 1630 { 1631 if( tester.meetsConstraints(correctedNode) && !nodeList.contains( correctedNode )) 1632 { 1633 nodeList.add( correctedNode ); 1634 } 1635 } 1636 } 1637 1638 1639 /** @param tP tracePair, or null if there is none. 1640 */ 1641 void setCorrecting( final boolean toCorrectResults, final CR_Vote.TracePair tP, 1642 final CorrectableCount count ) 1643 { 1644 if( isCorrecting != null ) throw new IllegalStateException(); 1645 1646 if( toCorrectResults && tP != null && tP.traceProjected != null ) 1647 { 1648 isCorrecting = true; 1649 count.castCorrection = tP.traceProjected[0].castVolume() 1650 - tP.traceAtLastCount[0].castVolume(); 1651 } 1652 else isCorrecting = false; 1653 } 1654 1655 private Boolean isCorrecting; 1656 1657 1658 /** A facility to test whether a node meets the contraints of a query. 1659 */ 1660 interface QueryConstraintTester{ public boolean meetsConstraints( CountNodeW n ); } 1661 1662 1663 // - C o u n t - T a b l e . P o l l - V i e w ------------------------------------ 1664 1665 1666 ArrayList<CountNodeW> sublistProperBaseCandidates() throws SQLException, XMLStreamException 1667 { 1668 final ArrayList<CountNodeW> nodeList = new ArrayList<CountNodeW>( 1669 /*initial capacity*/DART_SECTOR_MAX ); 1670 run( CountTable.BASE_CANDIDATE_TAIL + ' ' + CountTable.DART_SECTORED_TAIL, 1671 new CountNodeW.Runner() 1672 { public void run( final CountNodeW n ) { nodeList.add( n ); }} ); 1673 if( isCorrecting ) 1674 { 1675 correct( nodeList, new QueryConstraintTester() 1676 { 1677 public boolean meetsConstraints( final CountNodeW n ) 1678 { 1679 return n.isBaseCandidate() && n.dartSector() != 0; 1680 } 1681 }); 1682 } 1683 Collections.sort( nodeList, CountNodeW.DART_SECTOR_COMPARATOR ); 1684 return nodeList; 1685 } 1686 1687 1688 ArrayList<CountNodeW> sublistProperCasters( final String candidateEmail ) 1689 throws SQLException, XMLStreamException 1690 { 1691 final ArrayList<CountNodeW> nodeList = new ArrayList<CountNodeW>( 1692 /*initial capacity*/DART_SECTOR_MAX ); 1693 runCasters( candidateEmail, CountTable.DART_SECTORED_TAIL, new CountNodeW.Runner() 1694 { public void run( final CountNodeW n ) { nodeList.add( n ); }} ); 1695 if( isCorrecting ) 1696 { 1697 correct( nodeList, new QueryConstraintTester() 1698 { 1699 public boolean meetsConstraints( final CountNodeW n ) 1700 { 1701 return n.isCast() && candidateEmail.equals(n.getCandidateEmail()) 1702 && n.dartSector() != 0; 1703 } 1704 }); 1705 } 1706 Collections.sort( nodeList, CountNodeW.DART_SECTOR_COMPARATOR ); 1707 return nodeList; 1708 } 1709 1710 } 1711 1712 1713 1714 // ==================================================================================== 1715 1716 1717 private static final class Footnote 1718 { 1719 1720 Footnote( String _body ) { body = _body; } 1721 1722 1723 final String body; 1724 1725 1726 /** Constructs a new call link for this footnote, and adds it to the list. 1727 */ 1728 ExternalLink newCallLink() 1729 { 1730 final ExternalLink link = new ExternalLink( "link", new Model<String>(), 1731 new Model<String>() ); // models set later, in init_content.FOOTNOTES 1732 callLinkList.add( link ); 1733 return link; 1734 } 1735 1736 1737 final ArrayList<ExternalLink> callLinkList = new ArrayList<>( /*initial capacity*/2 ); 1738 1739 } 1740 1741 1742 1743 // ==================================================================================== 1744 1745 1746 private final class SpecificCrosspathBarFragment extends Fragment 1747 { 1748 1749 SpecificCrosspathBarFragment() 1750 { 1751 super( "candidateBar", "candidateBarFrag", WP_Votespace.this ); 1752 { 1753 final IModel<String> model = new AbstractReadOnlyModel<String>() 1754 { 1755 public String getObject() 1756 { 1757 // if( specificCrosspathNode == null ) return null; 1758 assert specificCrosspathNode != null; // else wasting time here: 1759 return VRequestCycle.get().bunW().l( 1760 "s.wic.count.WP_Votespace.candidateBar_XHT" ); 1761 } 1762 }; 1763 final Label label = new Label( "bar", model ) 1764 { 1765 public @Override boolean isVisible() { return isBarred(); } 1766 }; 1767 label.setEscapeModelStrings( false ); 1768 label.setRenderBodyOnly( true ); 1769 add( label ); 1770 } 1771 setRenderBodyOnly( true ); 1772 } 1773 1774 1775 transient Footnote footnote; // final after init 1776 1777 1778 void init( final FootnoteBuilder fB, final VRequestCycle cycle ) 1779 { 1780 if( initWasCalled ) throw new IllegalStateException(); 1781 1782 initWasCalled = true; 1783 final MarkupContainer sup; 1784 if( !isBarred() ) sup = newNullComponent( "sup" ); 1785 else 1786 { 1787 sup = new Fragment( "sup", "footnoteCallFrag", WP_Votespace.this ); 1788 footnote = new Footnote( "<p>" 1789 + cycle.bunA().l( "a.count.voteBar", 1790 IDPair.toUsername( specificCrosspathNode.email() ), 1791 IDPair.toUsername( specificCrosspathNode.getCandidateEmail() ), 1792 specificCrosspathNode.getBar() ) 1793 + "</p>" ); 1794 sup.add( footnote.newCallLink() ); 1795 fB.append( footnote ); 1796 } 1797 add( sup ); 1798 } 1799 1800 1801 boolean initWasCalled; 1802 1803 1804 private boolean isBarred() 1805 { 1806 return specificCrosspathNode != null && isVoterAndBarred( specificCrosspathNode ); 1807 } 1808 1809 } 1810 1811 1812}