package 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. import java.io.*; import java.sql.*; import java.text.*; import java.util.*; import java.util.Date; // over java.sql.Date import javax.script.*; import javax.xml.stream.XMLStreamException; import org.apache.wicket.*; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.markup.html.*; import org.apache.wicket.markup.html.basic.*; import org.apache.wicket.markup.html.form.*; import org.apache.wicket.markup.html.link.*; import org.apache.wicket.markup.html.panel.*; import org.apache.wicket.markup.repeater.*; import org.apache.wicket.model.*; import org.apache.wicket.protocol.http.PageExpiredException; import org.apache.wicket.request.cycle.*; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.util.string.StringValue; import votorola.a.*; import votorola.a.count.*; import votorola.a.position.*; import votorola.a.voter.*; import votorola.a.web.wic.*; import votorola.a.web.wic.authen.*; import votorola.g.*; import votorola.g.lang.*; import votorola.g.locale.*; import votorola.g.mail.*; import votorola.g.web.wic.*; import votorola.g.text.*; import static votorola.a.count.CountNode.DART_SECTOR_MAX; import static votorola.a.voter.IDPair.NOBODY; /** A view of the vote structure for a poll. A Crossforum Theatre {@linkplain * votorola.s.gwt.stage.StageV stage view} tops the page. The bulk of the page is * occupied by static HTML including a voting control and a navigable view of the vote * structure in cascading table form. For example: * *
http://reluk.ca:8080/v/w/Votespace?p=G!p!sandbox
* *

The particular poll is specified by query parameter 'p'. Query parameters for this * page are:

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Key ValueDefaultRecall
pThe name of the poll. * Slash characters (/) are technically not allowed here * and may therefore be encoded as exclamation marks (!).Null, resulting in a 303 (see other) redirect that fills in the name of * the {@linkplain Poll#TEST_POLL_NAME test poll}.yes
recallRedirectRecall parameters and redirect. The value is a set of recallable * parameter names, separated by vertical bars (e.g. 'p|u'). The server attempts * to recall the values from the context of recent requests, then responds with a * corrected URL in the form of a 303 redirect. Parameters that are specified * elsewhere in the request are not recalled in any case, and the values are * passed as specified into the corrected URL.Null, doing no redirection.no
uThe {@linkplain IDPair#username() username} of the person at the top of * the vote path. Incompatible with parameter 'v'; specify one or the * other.Null, specifying no particular person.yes
vThe {@linkplain IDPair#email() email address} of the person at the top of * the vote path. Incompatible with parameter 'u'; specify one or the * other.Null, specifying no particular person.yes
vCorWhether to correct the results for any vote shift of the user's since the * last reported count. A value of 'y' corrects the results, while 'n' leaves * them uncorrected.'y'no
* * @see WP_Votespace.html */ @ThreadRestricted("wicket") @org.apache.wicket.devutils.stateless.StatelessComponent public final class WP_Votespace extends VPageHTML implements TabbedPage, VoterPage { /** Constructs a WP_Votespace. */ public WP_Votespace( final PageParameters pP ) throws IOException, ScriptException, SQLException, XMLStreamException // bookmarkable page iff constructor public & (default|PageParameter) { super( pP ); final VRequestCycle cycle = VRequestCycle.get(); maybeRecallRedirect( WP_Votespace.class, pP, cycle ); final PollService poll = WP_Poll.ensurePoll( WP_Votespace.class, pP, cycle ); pollName = poll.name(); final VSession session = VSession.get(); session.scopePoll().setLastName( pollName ); setPageIcon( cycle.vRequest().getContextPath() + "/count/WP_Votespace/icon16.png" ); voterIDPair = VoterPage.U.idPairOrNobodyFor( pP ); session.scopeVoterPage().setLastIDPair( voterIDPair ); // Write glue for the GWT stage module of Crossforum Theatre // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final VoteServer vS = poll.vsRun().voteServer(); final VSession.User userOrNull = session.user(); { final StringBuilder b = WC_Stage.appendLeader( userOrNull, vS, cycle ); // s_gwt_stage_Stage_init, per WC_Stage below // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` b.append( "voGWTConfig.s_gwt_stage_Stage_init = function()" + "{" ); b.append( "s_gwt_stage_Stage_setActorName( " ); // rather than setDefaultActorName, which would disable absolute // deselection (the default being selected instead). We need // absolute deselection of base candidates for consistency with the // votespace scene. votorola.s.gwt.wic.PositionPager could detect a // deselection attempt that was disabled by a default setting if it // listened for the masked event, but that would be more complicated. // Changing? change also votorola.s.gwt.wic.CountIn.moduleLoad. if( NOBODY.equals( voterIDPair )) b.append( "null );" ); else b.append( '\'' ).append( voterIDPair.username() ).append( "' );" ); b.append( "s_gwt_stage_Stage_setDefaultPollName( '" ); b.append( pollName ).append( "' );" + "};" ); // ` ` ` add( new WC_Stage( "stage", "votorola.s.gwt.wic.CountIn", b, cycle )); } // Render view // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - add( new WC_NavigationHead( "navHead", WP_Votespace.this, cycle )); add( new WC_WGLogo( "wgLogo", poll.wgLogoImageLocation(), poll.wgLogoLinkTarget(), cycle )); { final String mapPageName = poll.divisionSmallMapPageName(); add( mapPageName == null? newNullComponent( "divisionSmallMap" ): new WC_DivisionSmallMap( "divisionSmallMap", poll.divisionPageName(), mapPageName, cycle )); } add( new WC_NavPile( "navPile", navTab(cycle), cycle )); try { init_content( vS, poll, userOrNull, cycle ); } catch( Exception x ) { throw VotorolaRuntimeException.castOrWrapped( x ); } setCacheable( true ); } @SuppressWarnings("deprecation") // IDPair.isFromEmail() private void init_content( final VoteServer vS, final PollService pollOrNull, final VSession.User userOrNull, final VRequestCycle cycle ) throws IOException, ScriptException, SQLException, XMLStreamException { final BundleFormatter bunW = cycle.bunW(); // POLL AND TITLING // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = final Model titleModel = new Model( bunW.l( "s.wic.count.WP_Votespace.title" )); add( new Label( "title", titleModel )); if( pollOrNull == null ) { assert false: "poll is never null"; // FIX clean up add( newNullComponent( "contentPoll" )); return; } final PollService poll = pollOrNull; titleModel.setObject( titleModel.getObject() + " - " + poll.name() ); final Fragment yPoll = newBodyOnlyFragment( "contentPoll", "contentPollFrag", WP_Votespace.this ); yPoll.add( new Label( "hName", poll.name() )); { final String displayTitle = poll.displayTitle(); if( displayTitle == null ) yPoll.add( newNullComponent( "hDisplayTitle" )); else yPoll.add( new Label( "hDisplayTitle", ": " + displayTitle )); } add( yPoll ); // CANDIDATE NAVIGATION AND VOTING CONTROLS // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = final CorrectableCount count; { final Count c = poll.countToReportT(); count = c == null? null: new CorrectableCount( c ); } final BundleFormatter bunA = cycle.bunA(); final String nobodyString = bunA.l( "a.count.nobodyEmailPlaceholder" ); final boolean isVotingEnabled; final String userEmail; if( userOrNull == null ) { isVotingEnabled = false; userEmail = null; currentVote = new Vote( NOBODY.email() ); } else { isVotingEnabled = true; userEmail = userOrNull.email(); currentVote = new Vote( userEmail, poll.voterInputTable() ); } newVote = currentVote.clone(); { final IDPair candidate; if( voterEmail().equals( userEmail )) { final String email = currentVote.getCandidateEmail(); candidate = email == null? NOBODY: new IDPair( email, IDPair.toUsername(email), /*isFromEmail*/voterIDPair.isFromEmail() ); // force to form as specified for voter } else if( NOBODY.equals( voterIDPair )) candidate = NOBODY; else candidate = voterIDPair; setNewCandidate( candidate ); // and thence newVote } final CandidateForm candidateForm = new CandidateForm(); yPoll.add( candidateForm ); { final TextField field = new TextField( "otherUID" ); candidateForm.add( field ); field.setModel( new PropertyModel( WP_Votespace.this, "newCandidate" ) { public @Override IDPair getObject() { final IDPair o = super.getObject(); return NOBODY.equals(o)? null: o; } public @Override void setObject( final IDPair o ) { super.setObject( o == null? NOBODY: o ); } }); invalidStyled( field ); IDPairConverter.setMaxLength_Type( field ); if( isVotingEnabled ) candidateForm.add( newNullComponent( "loginLink" )); else { final WC_LoginLink link = new WC_LoginLink( "loginLink", WP_Votespace.this, bunW.l( "s.wic.count.WP_Votespace.login" )); candidateForm.add( link ); field.setEnabled( count != null ); // no need of field, if all the buttons are disabled } } { final Button button = new Button( "go" ); button.add( AttributeModifier.replace( "value", bunW.l( "s.wic.count.WP_Votespace.candidateGo" ))); candidateForm.add( button ); button.setEnabled( count != null ); // no point in navigating anywhere, there is no view } { final Button button = new Button( "vote" ); button.add( AttributeModifier.replace( "value", bunW.l( "s.wic.count.WP_Votespace.candidateVote" ))); button.setEnabled( isVotingEnabled ); candidateForm.add( button ); } { final Button button = new Button( "unvote" ); button.add( AttributeModifier.replace( "value", bunW.l( "s.wic.count.WP_Votespace.candidateUnvote" ))); button.setEnabled( isVotingEnabled && currentVote.getCandidateEmail() != null ); candidateForm.add( button ); } // Feedback messages // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - candidateForm.add( new WC_Feedback( "feedback" )); // COUNT // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = // Summary view of count and user's vote (candidate detail). It is independent of // the vote path shown in the main cascade view (below). It therefore remains fixed // in height during path navigation, not causing the cascade view to shift // vertically. Keep it so. final Fragment yCount; final MarkupContainer candidateDetail; if( count == null ) { yCount = new Fragment( "contentCount", "contentCountNullFrag", WP_Votespace.this ); yCount.add( new Label( "explanation", bunA.l( "a.count.noResultsToReport" ))); yPoll.add( yCount ); if( isVotingEnabled ) { candidateDetail = new Fragment( "candidateDetail", "candidateLyCnFrag", WP_Votespace.this ); { final String candidateEmail = currentVote.getCandidateEmail(); final Label label = new Label("candidate", bunW.l( "s.wic.count.WP_Votespace.candidateLyCn", candidateEmail == null? nobodyString: IDPair.toUsername( candidateEmail ))); label.setRenderBodyOnly( true ); candidateDetail.add( label ); } } else candidateDetail = newNullComponent( "candidateDetail" ); candidateForm.add( candidateDetail ); return; } yCount = new Fragment( "contentCount", "contentCountFrag", WP_Votespace.this ); yPoll.add( yCount ); final CountTablePVC countTablePV = new CountTablePVC( count.countTable(), pollName ); final boolean toCorrectResults; // true iff a correction is actually needed final CountNodeW specificPathNodeAtLastCount; final SpecificCrosspathBarFragment specificCrosspathBarFragOrNull; if( isVotingEnabled ) { specificPathNodeAtLastCount = countTablePV.getOrCreate( userEmail ); final boolean nodeAtLastCountIsImageAndCurrent = specificPathNodeAtLastCount.isImage() && specificPathNodeAtLastCount.getTime() > currentVote.getTime(); final String candidateEmailOld = specificPathNodeAtLastCount.getCandidateEmail(); final String candidateEmailNew = currentVote.getCandidateEmail(); final AttributeAppenderS candidateLinkNewCrosspathStyler = newCandidateLinkCrosspathStyler(); if( nodeAtLastCountIsImageAndCurrent || ObjectX.nullEquals( candidateEmailOld, candidateEmailNew )) { toCorrectResults = false; final String candidateEmail = candidateEmailOld; // rather than new, which may be stale candidateDetail = new Fragment( "candidateDetail", "candidateLyCySnFrag", WP_Votespace.this ); { final Label label = new Label( "candidate1", bunW.l( "s.wic.count.WP_Votespace.candidateLyCySn1", candidateEmail == null? nobodyString: IDPair.toUsername( candidateEmail ))); label.setRenderBodyOnly( true ); candidateDetail.add( label ); } addCandidateDetail( candidateDetail, "s.wic.count.WP_Votespace.candidateLyCySn", 2, candidateEmail, candidateLinkNewCrosspathStyler, nobodyString, cycle ); } else { final String vCor = getPageParameters().get( "vCor" ).toString( "y" ); if( "y".equals( vCor )) toCorrectResults = true; else if( "n".equals( vCor )) toCorrectResults = false; else { VSession.get().error( "improper value for page parameter 'vCor': " + vCor ); throw new RestartResponseException( new WP_Message() ); } final AttributeAppenderS candidateLinkOldCrosspathStyler = newCandidateLinkCrosspathStyler(); candidateLinkOldCrosspathStyler.setEnabled( !toCorrectResults ); candidateLinkNewCrosspathStyler.setEnabled( toCorrectResults ); candidateDetail = new Fragment( "candidateDetail", "candidateLyCySyFrag", WP_Votespace.this ); addCandidateDetail( candidateDetail, "s.wic.count.WP_Votespace.candidateLyCySy", 1, candidateEmailOld, candidateLinkOldCrosspathStyler, nobodyString, cycle ); addCandidateDetail( candidateDetail, "s.wic.count.WP_Votespace.candidateLyCySy", 3, candidateEmailNew, candidateLinkNewCrosspathStyler, nobodyString, cycle ); { final Label label = new Label( "vCor", bunW.l( "s.wic.count.WP_Votespace.vCor." + vCor )); label.setRenderBodyOnly( true ); candidateDetail.add( label ); } { final PageParameters linkParameters = new PageParameters( getPageParameters() ); if( toCorrectResults ) linkParameters.set( "vCor", "n" ); else linkParameters.remove( "vCor" ); final BookmarkablePageLinkX link = new BookmarkablePageLinkX( "aModifier", WP_Votespace.class, linkParameters ); link.setBody( bunW.l( "s.wic.count.WP_Votespace.vCorUndo." + vCor )); candidateDetail.add( link ); } } specificCrosspathBarFragOrNull = new SpecificCrosspathBarFragment(); candidateDetail.add( specificCrosspathBarFragOrNull ); } else { toCorrectResults = false; specificPathNodeAtLastCount = null; candidateDetail = new Fragment( "candidateDetail", "candidateLnCyFrag", WP_Votespace.this ); specificCrosspathBarFragOrNull = null; } candidateForm.add( candidateDetail ); { final Fragment y = newBodyOnlyFragment( "countID", "countIDFrag", WP_Votespace.this ); candidateDetail.add( y ); y.add( new Label( "head", bunW.l("s.wic.count.WP_Votespace.candidateLnCy1") ) .setRenderBodyOnly(true) ); final ReadyDirectory ready = countTablePV.table().readyDirectory(); final File snap = ready.snapDirectory(); y.add( new ExternalLink( "a", /*href*/vS.votorolaURI().toASCIIString() + "/out/vocount/" + snap.getName() + "/" + ready.getName() + "/", /*body*/bunA.l( "a.OutputStore.setNominalDate", OutputStore.setNominalDate(new GregorianCalendar(),snap) ))); y.add( new Label( "tail", bunW.l("s.wic.count.WP_Votespace.candidateLnCy2") ) .setRenderBodyOnly(true) ); } final CountNodeW[] crosspath; { final CR_Vote.TracePair tP; if( toCorrectResults ) // show crosspath as current path { tP = new CR_Vote.TracePair( poll, count, currentVote, countTablePV ); if( tP.traceProjected == null ) { crosspath = new CountNodeW[] {}; specificCrosspathNode = null; } else { crosspath = tP.traceProjected; specificCrosspathNode = crosspath[0]; } } else // show crosspath as path at last count { tP = null; specificCrosspathNode = specificPathNodeAtLastCount; if( specificCrosspathNode == null ) crosspath = new CountNodeW[] {}; else crosspath = specificCrosspathNode.trace(); } countTablePV.setCorrecting( toCorrectResults, tP, count ); } final CountNodeW crosspathEndNode; final boolean crosspathEndNodeIsCandidate; if( specificCrosspathNode == null ) { crosspathEndNode = null; crosspathEndNodeIsCandidate = false; } else { crosspathEndNode = crosspath[crosspath.length - 1]; crosspathEndNodeIsCandidate = crosspathEndNode.isCandidate(); } // CASCADE MODEL // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = // A cascade is constructed of multiple tiers, arranged along a vote path, from // upstream (view left) to downstream (right). A vote path of length N nodes // corresponds to a cascade of either N tiers in depth, or N + 1 if there are // upstream voters off the path. final ArrayList tierList = new ArrayList(); final CountNodeW specificPathNode; final CountNodeW[] path; final CountNodeW pathEndNode; final boolean pathEndNodeIsCandidate; if( voterEmail().equals( NOBODY.email() )) { specificPathNode = null; path = new CountNodeW[] {}; pathEndNode = null; pathEndNodeIsCandidate = false; } else { final CountNodeW origin; if( voterEmail().equals( userEmail )) { origin = specificCrosspathNode; path = crosspath; } else { origin = countTablePV.getOrCreate( voterEmail() ); path = origin.trace(); } specificPathNode = origin; pathEndNode = path[path.length - 1]; pathEndNodeIsCandidate = pathEndNode.isCandidate(); titleModel.setObject( titleModel.getObject() + "/" + voterUsername() ); } // base tier, root candidates and cyclers // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { final CountNodeW pathNode = pathEndNodeIsCandidate? pathEndNode: null; final CountNodeW crosspathNode = crosspathEndNodeIsCandidate? crosspathEndNode: null; final Tier tier = new Tier( pathNode, crosspathNode, countTablePV.sublistProperBaseCandidates(), /*candidateNode*/null, count ); tierList.add( tier ); } { Tier orphanTier = null; if( path.length != 0 ) { // upstream tiers, voters // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( pathEndNodeIsCandidate ) { for( int p = path.length - 1; p >= 0; --p ) { final CountNodeW candidateNode = path[p]; final CountNodeW pathNode; // of voter if( p == 0 ) { if( !candidateNode.isCandidate() ) break; // top-most node has no voters pathNode = null; } else pathNode = path[p - 1]; final int q = p + crosspath.length - path.length; final CountNodeW crosspathNode; if( q > 0 && crosspath[q].equals( candidateNode )) // if share same candidate { crosspathNode = crosspath[q - 1]; } else crosspathNode = null; final Tier tier = new Tier( pathNode, crosspathNode, countTablePV.sublistProperCasters(candidateNode.email()), candidateNode, count ); assert toCorrectResults || tier.properNodesList.size() > 0; /* vote path length or candidacy implies voters, unless correcting and voter (not really counted yet) lacks dart sector */ tierList.add( 0, tier ); } } else // single non-voter/non-candidate orphan { assert path.length == 1; // non-candidate node in base tier implies path length 1 final Tier tier = new Tier( /*pathNode*/pathEndNode, pathEndNode ); tierList.add( 0, tier ); orphanTier = tier; } } } final int tN = tierList.size(); for( int t = 0; t < tN; ++t ) tierList.get(t).initPlace( specificPathNode, t, tN ); // CASCADE VIEW // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = // The view runs horizontally from the leftmost tier (upstream) to the rightmost // (downstream). Voters/candidates are stacked vertically in each tier. The layout // is a "cascading table". It is similar to a "cascading list", except that each // stacked node has multiple vertical columns for its various properties, such as // vote counts, email address, and links. int maxRowCount = 0; for( int t = 0; t < tN; ++t ) { final Tier tier = tierList.get( t ); final int rowCount = tier.rowCount(); if( rowCount > maxRowCount ) maxRowCount = rowCount; } { final RepeatingView tierRepeatingCol = new RepeatingView( "tierRepeatCol" ); yCount.add( tierRepeatingCol ); final RepeatingView tierRepeatingHead = new RepeatingView( "tierRepeatHead" ); yCount.add( tierRepeatingHead ); for( int t = 0, tLast = tN - 1; t <= tLast; ++t ) // left to right { // Columns row // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - tierRepeatingCol.add( newBodyOnlyFragment( tierRepeatingCol.newChildId(), "colRowFrag", WP_Votespace.this )); // Header row // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final CountNodeW candidateNode; if( t != tLast ) { final Tier candidateTier = tierList.get( t + 1 ); candidateNode = candidateTier.pathNode; } else candidateNode = null; final Fragment y = newBodyOnlyFragment( tierRepeatingHead.newChildId(), "headerRowFrag", WP_Votespace.this ); tierRepeatingHead.add( y ); // receive volume // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { final Label th = new Label( "receiveVolume", bunW.l( "s.wic.count.WP_Votespace.th.receiveCount.short" )); th.add( AttributeModifier.replace( "title", bunW.l( "s.wic.count.WP_Votespace.th.receiveCount" ))); y.add( th ); } // user mnemonic + label // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { final String key; if( t == tLast ) key = "s.wic.count.WP_Votespace.th.voterEmail.end"; else if( candidateNode == null ) { key = "s.wic.count.WP_Votespace.th.voterEmail.orphan"; } else key = "s.wic.count.WP_Votespace.th.voterEmail"; y.add( new Label( "voterEmail", bunW.l( key ))); } // hold volume // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { final WebMarkupContainer th = new WebMarkupContainer( "holdVolume" ); y.add( th ); th.add( AttributeModifier.replace( "title", bunW.l( "s.wic.count.WP_Votespace.th.holdCount" ))); th.add( new Label( "span", bunW.l( "s.wic.count.WP_Votespace.th.holdCount.short" ))); } // cast volume // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { final WebMarkupContainer th = new WebMarkupContainer( "castVolume" ); y.add( th ); th.add( AttributeModifier.replace( "title", bunW.l( "s.wic.count.WP_Votespace.th.singleCastCount" ))); th.add( new Label( "span", bunW.l( "s.wic.count.WP_Votespace.th.singleCastCount.short" ))); } // outflow volume // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { final WebMarkupContainer th = new WebMarkupContainer( "outflowVolume" ); y.add( th ); th.add( AttributeModifier.replace( "title", bunW.l( "s.wic.count.WP_Votespace.th.castCarryCount" ))); th.add( new Label( "span", bunW.l( "s.wic.count.WP_Votespace.th.castCarryCount.short" ))); } } } final RepeatingView rowRepeating = new RepeatingView( "repeat" ); yCount.add( rowRepeating ); final SimpleDateFormat iso8601Formatter = new SimpleDateFormat( SimpleDateFormatX.ISO_8601_PATTERN ); final Date date = new Date( 0L ); final StringBuilder b = new StringBuilder(); for( int r = 0; r < maxRowCount; ++r ) // data rows, top to bottom { final WebMarkupContainer row = new WebMarkupContainer( rowRepeating.newChildId() ); rowRepeating.add( row ); final RepeatingView tierRepeating = new RepeatingView( "tierRepeat" ); row.add( tierRepeating ); for( int t = 0, tLast = tN - 1; t <= tLast; ++t ) // left to right { final Tier tier = tierList.get( t ); final int lastNodeRow = tier.lastNodeRow(); final Tier candidateTier; final CountNodeW candidateNode; if( t != tLast ) { candidateTier = tierList.get( t + 1 ); candidateNode = candidateTier.pathNode; } else { candidateTier = null; candidateNode = null; } final Fragment y; // Footnote Row // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( r >= tier.footnoteRow ) { y = newBodyOnlyFragment( tierRepeating.newChildId(), "footnoteRowFrag", WP_Votespace.this ); // footnote // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( r == tier.footnoteRow ) { final WebMarkupContainer td = new WebMarkupContainer( "footnote" ); td.add( AttributeModifier.replace( "rowspan", Integer.toString( maxRowCount - r ))); tier.footnoteBuilder.td = td; y.add( td ); if( t < tLast && candidateNode == null ) appendStyleClass( td, "orphan" ); } else y.add( newNullComponent( "footnote" )); // outflow image // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` addOutflowImage( r, y, tier, candidateTier, cycle ); } // Sum row // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( tier.sumRow != -1 && r > tier.sumRow ) // it must be the 2nd sum row { y = newBodyOnlyFragment( tierRepeating.newChildId(), "sumRow2Frag", WP_Votespace.this ); // outflow image // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` addOutflowImage( r, y, tier, candidateTier, cycle ); } else if( r == tier.sumRow ) { // hold volume // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( t == tLast ) // base tier { y = newBodyOnlyFragment( tierRepeating.newChildId(), "sumHRowFrag", WP_Votespace.this ); y.add( new Label( "turnout", bunW.l( "s.wic.count.WP_Votespace.turnout" )).setRenderBodyOnly( true )); final Fragment sup = new Fragment( "sup", "footnoteCallFrag", WP_Votespace.this ); final long nTurnout = count.holdVolume(); final long nEligible = poll.populationSize(); final String footnoteBody; if( nEligible > 0 ) { footnoteBody = bunW.l( "s.wic.count.WP_Votespace.turnout_XHT", nTurnout, nEligible, nTurnout * 100d / nEligible ); } else footnoteBody = bunW.l( "s.wic.count.WP_Votespace.turnout0_XHT" ); // turnout cannot be calculated final Footnote footnote = new Footnote( footnoteBody ); sup.add( footnote.newCallLink() ); tier.footnoteBuilder.append( footnote ); y.add( sup ); y.add( new Label( "holdVolume", bunA.format( "%,d", nTurnout ))); } // outflow volume // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` else // voter tier { y = newBodyOnlyFragment( tierRepeating.newChildId(), "sumCCRowFrag", WP_Votespace.this ); y.add( new Label( "outflowVolume", bunA.format( "%,d", candidateNode.receiveVolume() ))); } // outflow image // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` addOutflowImage( r, y, tier, candidateTier, cycle ); } // Other row // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( r == tier.otherNodesRow ) { y = newBodyOnlyFragment( tierRepeating.newChildId(), "otherNodesRowFrag", WP_Votespace.this ); // user mnemonic + label // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final long holdVolume = tier.otherNodesCumulate.holdVolume; y.add( new Label( "other", bunW.l( t == tLast && holdVolume == Tier.CountCumulate.NO_PARTICIPANTS? "s.wic.count.WP_Votespace.otherNodes0": "s.wic.count.WP_Votespace.otherNodes" ))); // hold count // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` y.add( new Label( "holdVolume", holdVolume == -1? "": // non-base tier, cumulative data not calculated bunA.format( "%,d", holdVolume ))); // outflow volume // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final long outflowVolume = tier.otherNodesCumulate.outflowVolume; y.add( new Label( "outflowVolume", outflowVolume == -1? "": // base tier, cumulative data not calculated bunA.format( "%,d", outflowVolume ))); // outflow image // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` addOutflowImage( r, y, tier, candidateTier, cycle, /*node*/null ); } // Node row // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else { y = newBodyOnlyFragment( tierRepeating.newChildId(), "nodeRowFrag", WP_Votespace.this ); final CountNodeW node = tier.getNode( r ); final String nodeUsername = node.person().username(); final boolean isPathNode; final boolean isSpecificPathNode; if( r == tier.pathRow ) { isPathNode = true; isSpecificPathNode = node.equals( specificPathNode ); } else { isPathNode = false; isSpecificPathNode = false; } // inflow // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { final WebMarkupContainer td = new WebMarkupContainer( "inflow" ); y.add( td ); final Component img; if( isPathNode && node.isCandidate() ) { appendStyleClass( td, "f" ); // flow appendStyleClass( td, "i" ); // in img = new WebMarkupContainer( "img" ); final String imgName = isSpecificPathNode? "f-i": "f-i-path"; img.add( AttributeModifier.replace( "src", cycle.vRequest().getContextPath() + "/count/WP_Votespace/" + imgName + ".png" )); } else img = newNullComponent( "img" ); td.add( img ); } // receive volume // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final Label rTD = new Label( "receiveVolume", bunA.format("%,d",node.receiveVolume()) ); y.add( rTD ); if( isPathNode ) appendStyleClass( rTD, "dpath" ); // user mnemonic // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final Label voterEmailSpan; { final WebMarkupContainer td = new WebMarkupContainer( "voterEmail" ); y.add( td ); if( r == tier.crosspathRow ) appendStyleClass( td, "crosspath" ); voterEmailSpan = new Label( "span", IDPair.buildUserMnemonic(nodeUsername,StringBuilderX.clear(b)) .toString() ); td.add( voterEmailSpan ); } // display title // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final long castVolume = node.castVolume(); final long outflowVolume = castVolume + node.carryVolume(); { final boolean isOrphan = t < tLast && !node.isVoter(); final boolean toEnable = !isOrphan; // otherwise click on node and it vanishes (too surprising) final PageParameters linkParameters = new PageParameters( getPageParameters() ); linkParameters.remove( "v" ); if( isSpecificPathNode ) // leftmost on path { if( path.length == 1 ) linkParameters.remove( "u" ); // clear single-node path to no path at all else // else unroll the path, rightward { linkParameters.set( "u", IDPair.toUsername( node.getCandidateEmail() )); } } else linkParameters.set( "u", nodeUsername ); // make it the specific node final WebMarkupContainer td; { if( toEnable ) { td = new BookmarkablePageLinkX( // JavaScript link "displayTitle", WP_Votespace.class, linkParameters ); appendStyleClass( td, "k" ); // clickable if( tier.place != XCastRelation.UNKNOWN ) { td.add( new AttributeModifier( "id", tier.place.symbol() + Byte.toString(node.dartSector()) )); td.add( new AttributeModifier( "onmouseover", "_s_wic_count_WP_Votespace.dartSpotOn(this)" )); td.add( new AttributeModifier( "onmouseout", "_s_wic_count_WP_Votespace.dartSpotOff(this)" )); } } else td = new WebMarkupContainer( "displayTitle" ); y.add( td ); } if( isPathNode ) appendStyleClass( td, "dpath" ); final BookmarkablePageLinkX link = new BookmarkablePageLinkX( // ordinary link nested in JavaScript link "a", WP_Votespace.class, linkParameters ); final String displayTitle = node.displayTitle(); final String linkBody; if( displayTitle == null ) linkBody = nodeUsername; else { linkBody = displayTitle; appendStyleClass( td, "dt" ); appendStyleClass( rTD, "dt" ); voterEmailSpan.add( AttributeModifier.replace( "title", nodeUsername )); } link.setBody( linkBody ); link.setEnabled( toEnable ); td.add( link ); if( isOrphan ) { final Footnote footnote = new Footnote( bunW.l( "s.wic.count.WP_Votespace.orphanVoter-non_XHT" )); tier.footnoteBuilder.append( footnote ); final Fragment sup = new Fragment( "sup", "footnoteCallFrag", WP_Votespace.this ); sup.add( footnote.newCallLink() ); td.add( sup ); } else if( isVoterAndBarred( node )) { final Footnote footnote; if( node.equals( specificCrosspathNode )) { specificCrosspathBarFragOrNull.init( tier.footnoteBuilder, cycle ); footnote = specificCrosspathBarFragOrNull.footnote; if( footnote == null ) throw new NullPointerException(); // fail fast } else { footnote = new Footnote( "

" + bunA.l( "a.count.voteBar", nodeUsername, IDPair.toUsername( node.getCandidateEmail() ), node.getBar() ) + "

" ); tier.footnoteBuilder.append( footnote ); } final Fragment sup = new Fragment( "sup", "footnoteCallFrag", WP_Votespace.this ); sup.add( footnote.newCallLink() ); td.add( sup ); } else td.add( newNullComponent( "sup" )); } // hold volume // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { final Label td = new Label( "holdVolume", bunA.format("%,d",node.holdVolume()) ); y.add( td ); if( isPathNode ) appendStyleClass( td, "dpath" ); } // cast volume // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { final Model href = new Model(); final ExternalLink a = new ExternalLink( "castVolume", href, /*label*/new Model(bunA.format("%,d",castVolume)) ); y.add( a ); if( castVolume == 0L ) a.setEnabled( false ); // no vote else { final String source = node.getSource(); if( source == null ) { href.setObject( vS.votorolaURI().toASCIIString() + "/out/vocount/_snap_report/_in_vote/" + pollName + ".xml" ); } else // vote is mirror image { href.setObject( vS.votorolaURI().toASCIIString() + "/in/vomir/" + source + "/_snap_current/" + pollName + ".xml" ); appendStyleClass( a, "mir" ); } } } // outflow volume // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { final Label td = new Label( "outflowVolume", bunA.format("%,d",outflowVolume) ); y.add( td ); if( isPathNode ) appendStyleClass( td, "dpath" ); } // outflow image // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` addOutflowImage( r, y, tier, candidateTier, cycle, node ); } tierRepeating.add( y ); } // startCol and endCol (first row only) // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( r == 0 ) { final AttributeModifier sAM = AttributeModifier.replace( "rowspan", Integer.toString( maxRowCount )); { final WebMarkupContainer td = new WebMarkupContainer( "startCol" ); td.add( sAM ); row.add( td ); } { final WebMarkupContainer td = new WebMarkupContainer( "endCol" ); td.add( sAM ); row.add( td ); } } else { row.add( newNullComponent( "startCol" )); row.add( newNullComponent( "endCol" )); } } // FOOTNOTES // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = if( specificCrosspathBarFragOrNull != null && !specificCrosspathBarFragOrNull.initWasCalled ) // i.e. footnote not placed in orphan tier { specificCrosspathBarFragOrNull.init( tierList.get(tN-1).footnoteBuilder, cycle ); // place in final tier } for( int f = 0, t = 0; t < tN; ++t ) { final Tier tier = tierList.get( t ); final FootnoteBuilder fB = tier.footnoteBuilder; final MarkupContainer container; if( fB.list.size() == 0 ) container = newNullComponent( "container" ); else { appendStyleClass( fB.td, "footnote" ); container = new Fragment( "container", "footnoteTableFrag", WP_Votespace.this ); final RepeatingView repeating = new RepeatingView( "repeat" ); container.add( repeating ); final int fTN = fB.list.size(); for( int fT = 0;; ) { final Footnote footnote = fB.list.get( fT ); final WebMarkupContainer y = new WebMarkupContainer( repeating.newChildId() ); repeating.add( y ); ++f; // final String fString = Integer.toString( f ); /// but letters are better in a numeric display, so try this hack: final String fString = Integer.toString( f + 9, /*radix*/36 ); // final String noteLinkHref = "#fc-" + fString; /// (a) it's not helpful, so defeat actual linking: final String noteLinkHref = null; final ExternalLink noteLink = new ExternalLink( "link", noteLinkHref, fString ); noteLink.add( AttributeModifier.replace( "id", "fn-" + fString )); y.add( noteLink ); for( int c = footnote.callLinkList.size() - 1;; --c ) { final ExternalLink callLink = footnote.callLinkList.get( c ); // callLink.setDefaultModelObject( "#fn-" + fString ); // defeated per (a) above IModelX.setObject( callLink.getBody(), fString ); if( c == 0 ) { callLink.add( AttributeModifier.replace( "id", "fc-" + fString )); // only the first call gets this break; } } y.add( new Label( "body", footnote.body ).setEscapeModelStrings( false )); ++fT; if( fT >= fTN ) { appendStyleClass( y, "last" ); break; } } } fB.td.add( container ); } } // ------------------------------------------------------------------------------------ /** Effects a recall-redirect if requested. Any parameters named by a * 'recallRedirect' parameter are recalled and a 303 redirect exception is thrown. */ static void maybeRecallRedirect( final Class pageClass, final PageParameters pP, final VRequestCycle cycle ) { // final String value = (String)p.remove( "recallRedirect" ); /// but Wicket 1.3 is storing parameter values as arrays, and not documenting it, so this is easier: final String value = pP.get( "recallRedirect" ).toString(); if( value == null ) return; pP.remove( "recallRedirect" ); final Set pOld = pP.getNamedKeys(); final Set pNew = PageParametersX.splitAsSet( value, PageParametersX.SPLIT_ON_BAR_PATTERN ); if( pNew.contains( "p" ) && !pOld.contains( "p" )) WP_Poll.withRecall_p( pP ); if( (pNew.contains( "u" ) || pNew.contains( "v" )) && !pOld.contains( "u" ) && !pOld.contains( "v" )) VoterPage.U.withRecall_u_v( pP ); throw new RedirectException( cycle.uriFor(pageClass,pP).toASCIIString(), 303 ); } /** Returns the query parameters for a particular votespace page. The voter is set as * the last fore-navigated to. * * @see votorola.a.voter.VoterPage.SessionScope#getLastIDPair() */ public static PageParameters parameters( final String serviceName, final VRequestCycle cycle ) { final PageParameters pP = new PageParameters(); pP.set( "p", Poll.U.toQuery( serviceName )); return VoterPage.U.withRecall_u_v( pP ); } public @Warning("non-API") IDPair getNewCandidate() { return newCandidate; }; private IDPair newCandidate; public @Warning("non-API") void setNewCandidate( IDPair _newCandidate ) // public for sake of Wicket property models only { newCandidate = _newCandidate; newVote.setCandidateEmail( NOBODY.equals(_newCandidate)? null: _newCandidate.email() ); }; // - T a b b e d - P a g e ------------------------------------------------------------ /** @see #NAV_TAB */ public NavTab navTab( VRequestCycle cycle ) { return NAV_TAB; } /** The navigation tab that fetches the votespace page, an instance of WP_Votespace. */ public static final NavTab NAV_TAB = new VotespaceTab( WP_Votespace.class ) { public @Override String shortTitle( final VRequestCycle cycle ) { return cycle.bunW().l( "s.wic.count.WP_Votespace.tab.shortTitle" ); } }; // - V o t e r - P a g e -------------------------------------------------------------- public String voterEmail() { return voterIDPair.email(); } public IDPair voterIDPair() { return voterIDPair; } private final IDPair voterIDPair; public String voterUsername() { return voterIDPair.username(); } // ==================================================================================== /** An aid for constructing a set of footnotes. */ static final class FootnoteBuilder { /** Appends a footnote to the set. */ void append( final Footnote footnote ) { if( list.size() == 0 ) list = new ArrayList( /*init capacity*/4 ); list.add( footnote ); } /** The read-only list of footnotes. To append footnotes to the list, use * append(). */ List list = Collections.emptyList(); /** The footnote cell at the bottom of the tier, or null if the footnotes are to * be placed outside of any tier. A single set of footnotes is placed outside of * a tier when there is no count. */ WebMarkupContainer td; } // ==================================================================================== static abstract @ThreadSafe class VotespaceTab extends NavTab { /** Contructs a VotespaceTab. */ VotespaceTab( Class _pageClass ) { pageClass = _pageClass; } // - N a v - T a b ---------------------------------------------------------------- public @Override final Bookmark bookmark() { PageParameters pP = null; pP = WP_Poll.withRecall_p( pP ); pP = VoterPage.U.withRecall_u_v( pP ); return new Bookmark( pageClass, pP ); } public @Override Class pageClass() { return pageClass; } private final Class pageClass; } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private void addCandidateDetail( final MarkupContainer y, final String baseKey, int suffix, final String email, final AttributeAppender styler, final String nobodyString, final VRequestCycle cycle ) { y.add( newCandidateDetailLabel( baseKey, suffix, cycle )); y.add( newCandidateDetailVLink( suffix, email, nobodyString ).add( styler )); ++suffix; y.add( newCandidateDetailLabel( baseKey, suffix, cycle )); } /** Adds outflow for a non-node row. */ private void addOutflowImage( final int r, final WebMarkupContainer y, final Tier tier, final Tier candidateTier, final VRequestCycle cycle ) { final WebMarkupContainer td = new WebMarkupContainer( "outflow" ); y.add( td ); Component img = null; // so far if( candidateTier != null ) { int rCandidate = candidateTier.pathRow; if( r <= rCandidate ) { appendStyleClass( td, "f" ); // flow appendStyleClass( td, "o" ); // out if( tier.pathNode != null ) appendStyleClass( td, "path" ); if( r == rCandidate ) { img = new WebMarkupContainer( "img" ); final StringBuilder b = new StringBuilder(); b.append( cycle.vRequest().getContextPath() ); b.append( "/count/WP_Votespace/f-DR-clear-bottom" ); if( tier.pathNode != null && ( r == tier.pathRow || r == rCandidate )) b.append( "-path" ); b.append( ".png" ); img.add( AttributeModifier.replace( "src", b.toString() )); } // else empty cell, showing only the background image } } if( img == null ) img = newNullComponent( "img" ); td.add( img ); } /** Adds outflow for a node row (proper, other, or external path). * * @param node the count node, or null in the case of an "other" row. */ private void addOutflowImage( final int r, final WebMarkupContainer y, final Tier tier, final Tier candidateTier, final VRequestCycle cycle, final CountNodeW node ) { final WebMarkupContainer td = new WebMarkupContainer( "outflow" ); y.add( td ); final MarkupContainer img; if( candidateTier == null ) img = newNullComponent( "img" ); else if( node == null/*other row*/ || node.isVoter() ) { int rCandidate = candidateTier.pathRow; appendStyleClass( td, "f" ); // flow appendStyleClass( td, "o" ); // out if( tier.pathNode != null ) { if( r > tier.pathRow && r <= rCandidate || r <= tier.pathRow && r > rCandidate ) appendStyleClass( td, "path" ); } img = new WebMarkupContainer( "img" ); final int lastNodeRow = tier.lastNodeRow(); final StringBuilder b = new StringBuilder(); b.append( cycle.vRequest().getContextPath() ); b.append( "/count/WP_Votespace/f-" ); if( r == 0 ) { appendStyleClass( td, "top" ); if( r == rCandidate ) { if( r == lastNodeRow ) b.append( "R-single" ); else if( tier.pathNode == null ) b.append( "UR-top" ); // covers everything in this case, and so there are no other images else if( r == tier.pathRow ) b.append( "R-top" ); else b.append( "UR-top" ); } else b.append( "RD-top" ); } else if( r < rCandidate ) { if( r == tier.pathRow ) b.append( "RD-top" ); else b.append( "RD" ); // special case } else if( r == lastNodeRow ) { if( r == rCandidate ) b.append( "DR-bottom" ); else b.append( "RU-bottom" ); } else { if( r == rCandidate ) { if( tier.pathNode == null ) b.append( "R" ); // covers everything in this case, and so there are no other images else if( r > tier.pathRow ) b.append( "DR" ); else if( r < tier.pathRow ) b.append( "UR" ); else b.append( 'R' ); } else b.append( "RU" ); } if( tier.pathNode != null && ( r == tier.pathRow || r == rCandidate )) b.append( "-path" ); b.append( ".png" ); img.add( AttributeModifier.replace( "src", b.toString() )); } else // non-voter { appendStyleClass( td, "f" ); // flow appendStyleClass( td, "o" ); // out appendStyleClass( td, "non" ); img = new WebMarkupContainer( "img" ); img.add( AttributeModifier.replace( "src", cycle.vRequest().getContextPath() + "/count/WP_Votespace/f-non.png" )); } td.add( img ); } /** @see #newVote */ private Vote currentVote; // final after init /** Return true iff the node is both voting and barred. The presence of a bar is not * enough to test for this condition, because non-voting nodes are not checked for * bars during the count (an optimization), but instead are left with the pseudo-bar * "voterBarUnknown". Hence this added complexity. */ private static boolean isVoterAndBarred( final CountNodeW node ) { return node.isCast() && node.getBar() != null; } private Label newCandidateDetailLabel( final String baseKey, final int suffix, final VRequestCycle cycle ) { final Label label = new Label( "candidate" + suffix, cycle.bunW().l( baseKey + suffix )); label.setRenderBodyOnly( true ); return label; } private BookmarkablePageLinkX newCandidateDetailVLink( final int suffix, final String email, final String nobodyString ) { final PageParameters linkParameters = new PageParameters( getPageParameters() ); linkParameters.remove( "v" ); boolean toEnableLink = false; // so far final String username; if( email == null ) { username = nobodyString; linkParameters.remove( "u" ); toEnableLink = !voterEmail().equals( NOBODY.email() ); } else if( email.equals( voterEmail() )) username = voterUsername(); else { username = IDPair.toUsername( email ); linkParameters.set( "u", username ); toEnableLink = true; } final BookmarkablePageLinkX link = new BookmarkablePageLinkX( "a" + suffix, WP_Votespace.class, linkParameters ); link.setBody( username ); link.setEnabled( toEnableLink ); return link; } private static AttributeAppenderS newCandidateLinkCrosspathStyler() { return new AttributeAppenderS( "class", new Model("crosspath"), " " ); } /** @see #currentVote */ private Vote newVote; // final after init, don't set candidate directly, but use setNewCandidate private static final String[] pKey = { "p", "recallRedirect", "u", "v", "vCor" }; // known parameters private final String pollName; private CountNodeW specificCrosspathNode; // or null, final after init // ==================================================================================== private final class CandidateForm extends StatelessForm { private CandidateForm() { super( "candidate" ); } protected @Override void onSubmit() { super.onSubmit(); // if( !isVotingEnabled ) throw new IllegalStateException(); // probably impossible //// no need, access is guarded in VoterInputTable final VRequestCycle cycle = VRequestCycle.get(); final Component submitter = (Component)findSubmittingButton(); // Go // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( submitter == null || "go".equals( submitter.getId() )) { setResponsePage( VRequestCycle.get() ); return; } // Vote or Unvote // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( VSession.get().user() == null ) throw new PageExpiredException( /*message*/null ); // fail fast, user has logged out and navigated back to this page if( "unvote".equals( submitter.getId() )) setNewCandidate( NOBODY ); // and thence newVote else assert "vote".equals( submitter.getId() ); try { final PollService poll = WP_Poll.pollFor( pollName, cycle ); newVote.write( poll.voterInputTable(), VSession.get(), /*toForce*/true ); VOWicket.get().scopeActivity().activityList().log( poll.newChangeEventOrNull( currentVote, newVote )); setResponsePage( cycle ); } catch( Exception x ) { throw VotorolaRuntimeException.castOrWrapped( x ); } } private void setResponsePage( final VRequestCycle cycle ) { final PageParameters oldP = getPageParameters(); final PageParameters newP = new PageParameters(); for( final String key: pKey ) // strip stateless form's submission parameters { final List values = oldP.getValues( key ); // though there's only ever the one in this case for( final StringValue v: values ) newP.add( key, v ); } VoterPage.U.setFrom( newCandidate, newP ); cycle.setResponsePage( WP_Votespace.class, newP ); } } // ==================================================================================== private static @ThreadRestricted final class CorrectableCount extends Count { CorrectableCount( final Count count ) { super( count ); } private long castCorrection; // - C o u n t -------------------------------------------------------------------- public long castVolume() { return super.castVolume() + castCorrection; } } // ==================================================================================== /** A cached view of a count table restricted to a particular poll. */ private static @ThreadRestricted final class CountTablePVC extends CR_Vote.CountTablePVC { CountTablePVC( CountTable t, String serviceName ) { super( t, serviceName ); } private void correct( final ArrayList nodeList, final QueryConstraintTester tester ) { // Substitute changed nodes, remove any that no longer meet query contraints // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final ListIterator nodeListI = nodeList.listIterator(); while( nodeListI.hasNext() ) { final CountNodeW node = nodeListI.next(); final CountNodeW correctedNode = cache.get( node.email() ); if( correctedNode == null ) continue; // node unaffected by vote shift if( tester.meetsConstraints( correctedNode )) nodeListI.set( correctedNode ); else nodeListI.remove(); } } // Add changed nodes that now meet query contraints // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - for( final CountNodeW correctedNode: cache.values() ) { if( tester.meetsConstraints(correctedNode) && !nodeList.contains( correctedNode )) { nodeList.add( correctedNode ); } } } /** @param tP tracePair, or null if there is none. */ void setCorrecting( final boolean toCorrectResults, final CR_Vote.TracePair tP, final CorrectableCount count ) { if( isCorrecting != null ) throw new IllegalStateException(); if( toCorrectResults && tP != null && tP.traceProjected != null ) { isCorrecting = true; count.castCorrection = tP.traceProjected[0].castVolume() - tP.traceAtLastCount[0].castVolume(); } else isCorrecting = false; } private Boolean isCorrecting; /** A facility to test whether a node meets the contraints of a query. */ interface QueryConstraintTester{ public boolean meetsConstraints( CountNodeW n ); } // - C o u n t - T a b l e . P o l l - V i e w ------------------------------------ ArrayList sublistProperBaseCandidates() throws SQLException, XMLStreamException { final ArrayList nodeList = new ArrayList( /*initial capacity*/DART_SECTOR_MAX ); run( CountTable.BASE_CANDIDATE_TAIL + ' ' + CountTable.DART_SECTORED_TAIL, new CountNodeW.Runner() { public void run( final CountNodeW n ) { nodeList.add( n ); }} ); if( isCorrecting ) { correct( nodeList, new QueryConstraintTester() { public boolean meetsConstraints( final CountNodeW n ) { return n.isBaseCandidate() && n.dartSector() != 0; } }); } Collections.sort( nodeList, CountNodeW.DART_SECTOR_COMPARATOR ); return nodeList; } ArrayList sublistProperCasters( final String candidateEmail ) throws SQLException, XMLStreamException { final ArrayList nodeList = new ArrayList( /*initial capacity*/DART_SECTOR_MAX ); runCasters( candidateEmail, CountTable.DART_SECTORED_TAIL, new CountNodeW.Runner() { public void run( final CountNodeW n ) { nodeList.add( n ); }} ); if( isCorrecting ) { correct( nodeList, new QueryConstraintTester() { public boolean meetsConstraints( final CountNodeW n ) { return n.isCast() && candidateEmail.equals(n.getCandidateEmail()) && n.dartSector() != 0; } }); } Collections.sort( nodeList, CountNodeW.DART_SECTOR_COMPARATOR ); return nodeList; } } // ==================================================================================== private static final class Footnote { Footnote( String _body ) { body = _body; } final String body; /** Constructs a new call link for this footnote, and adds it to the list. */ ExternalLink newCallLink() { final ExternalLink link = new ExternalLink( "link", new Model(), new Model() ); // models set later, in init_content.FOOTNOTES callLinkList.add( link ); return link; } final ArrayList callLinkList = new ArrayList<>( /*initial capacity*/2 ); } // ==================================================================================== private final class SpecificCrosspathBarFragment extends Fragment { SpecificCrosspathBarFragment() { super( "candidateBar", "candidateBarFrag", WP_Votespace.this ); { final IModel model = new AbstractReadOnlyModel() { public String getObject() { // if( specificCrosspathNode == null ) return null; assert specificCrosspathNode != null; // else wasting time here: return VRequestCycle.get().bunW().l( "s.wic.count.WP_Votespace.candidateBar_XHT" ); } }; final Label label = new Label( "bar", model ) { public @Override boolean isVisible() { return isBarred(); } }; label.setEscapeModelStrings( false ); label.setRenderBodyOnly( true ); add( label ); } setRenderBodyOnly( true ); } transient Footnote footnote; // final after init void init( final FootnoteBuilder fB, final VRequestCycle cycle ) { if( initWasCalled ) throw new IllegalStateException(); initWasCalled = true; final MarkupContainer sup; if( !isBarred() ) sup = newNullComponent( "sup" ); else { sup = new Fragment( "sup", "footnoteCallFrag", WP_Votespace.this ); footnote = new Footnote( "

" + cycle.bunA().l( "a.count.voteBar", IDPair.toUsername( specificCrosspathNode.email() ), IDPair.toUsername( specificCrosspathNode.getCandidateEmail() ), specificCrosspathNode.getBar() ) + "

" ); sup.add( footnote.newCallLink() ); fB.append( footnote ); } add( sup ); } boolean initWasCalled; private boolean isBarred() { return specificCrosspathNode != null && isVoterAndBarred( specificCrosspathNode ); } } }