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}