001package votorola.s.gwt.scene.vote; // Copyright 2011, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.
002
003import com.google.gwt.core.client.*;
004import com.google.gwt.dom.client.Element;
005import com.google.gwt.event.dom.client.*;
006import com.google.gwt.user.client.Window;
007import com.google.gwt.user.client.ui.HTML;
008import com.google.gwt.user.client.ui.RootPanel;
009import com.google.gwt.view.client.*;
010import com.google.web.bindery.event.shared.HandlerRegistration;
011import org.vectomatic.dom.svg.*;
012import org.vectomatic.dom.svg.utils.*;
013import votorola.a.count.gwt.*;
014import votorola.a.web.gwt.*;
015import votorola.g.hold.*;
016import votorola.g.lang.*;
017import votorola.g.web.gwt.*;
018import votorola.g.web.gwt.event.*;
019import votorola.g.web.gwt.svg.*;
020import votorola.s.gwt.scene.*;
021import votorola.s.gwt.stage.*;
022import votorola.s.gwt.stage.link.*;
023
024import static org.vectomatic.dom.svg.OMSVGLength.SVG_LENGTHTYPE_PX;
025
026
027/** A view of a {@linkplain Votespace votespace} in which the main component is
028  * implemented as a scalable vector graphic.  The overall layout of the page is as
029  * follows:<pre>
030  *
031  *    +--------------------------------------------------------+
032  *    |                        stage                           |
033  *    +-------------+------------------------------------------+
034  *    |             |                                          |
035  *    |             |                                          |
036  *    |             |                                          |
037  *    |             |                                          |
038  *    |             |                                          |
039  *    |    feed     |                                          |
040  *    |             |                  svg                     |
041  *    |             |                                          |
042  *    |             |                                          |
043  *    |             |                                          |
044  *    |             |                                          |
045  *    |             |                                          |
046  *    +-------------+------------------------------------------+</pre>
047  *
048  * <p>Except where noted otherwise in the API, lengths for the vector graphic (svg) are
049  * given in "em" units.  Angles are given in degrees measured clockwise from the positive
050  * (right hand) x axis.</p>
051  *
052  * <p>Acknowledgement: The design of this view follows from the suggestions of Thomas von
053  * der Elbe, in a Skype discussion on April 11, 2011.  See the Metagovernment <a
054  * href='http://reluk.ca/var/cache/irc/metagov/11-04/11' target='_top'>IRC log</a> and
055  * the <a
056  * href='http://metagovernment.org/pipermail/start_metagovernment.org/2011-April/003819.html'
057  * target='_top'>follow up post</a> to the Metagovernment mailing list.</p>
058  *
059  *     @see votorola.s.gwt.stage.StageIn
060  *     @see <a href='http://reluk.ca/y/vw/xf/#c=DV&s=G!p!sandbox' target='_top'>Live
061  *       example of a VotespaceV (right)</a>
062  */
063  @Warning("permanently disabled on detach from document")
064public final class VotespaceV extends HTML implements SVGNest<SVGNest<?>>
065{
066
067    // Note: Intermediate calculations are done in floats only because the SVG library
068    // dislikes doubles.
069
070
071    /** Constructs a VotespaceV.
072      */
073    public VotespaceV( Votespace _model )
074    {
075        model = _model;
076
077        final Element y = getElement();
078        y.addClassName( "vote-VotespaceV" );
079
080      // Resource accounting view.
081      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
082        if( Scenes.toUseRAC() )
083        {
084            final LinkTrackV linkTrackV = LinkTrackV.i( StageV.i() );
085            if( linkTrackV != null )
086            {
087                final SacSelectionV sacSelectionV = new SacSelectionV( model );
088                linkTrackV.addLeftTool( sacSelectionV );
089                spool.add( new Hold()
090                {
091                    public void release() { sacSelectionV.removeFromParent(); }
092                });
093            }
094        }
095
096      // Votespace SVG.
097      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
098        final Element yy = new HTML().getElement();
099        y.appendChild( yy );
100        yy.addClassName( "insulator" );
101        yy.appendChild( svg.getElement() );
102
103      // Controllers.
104      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
105        pathExtender = new PathExtender();
106        new Modeller();
107    }
108
109
110
111   // ````````````````````````````````````````````````````````````````````````````````````
112   // init for early use
113
114
115    private final OMSVGDocument document = OMSVGParser.createDocument();
116
117
118    private final OMSVGSVGElement svg = document.createSVGSVGElement();
119
120
121
122   // ------------------------------------------------------------------------------------
123
124
125    /** Appends the specified length to the length list.  This is a convenience method.
126      */
127    void append( final OMSVGAnimatedLengthList lengthList, final float length )
128    {
129        lengthList.getBaseVal().appendItem( svg.createSVGLength( SVG_LENGTHTYPE_PX,
130          length * pxPerEm ));
131    }
132
133
134
135    /** The document of which the underlying SVG view is a component.
136      *
137      *     @see #svgView()
138      */
139    OMSVGDocument document() { return document; }
140
141
142
143    /** The votespace model on which this view is based.
144      */
145    Votespace model() { return model; }
146
147
148        private final Votespace model;
149
150
151
152    /** Divisor for the threshold of votes below which a node is considered a "mosquito".
153      * This applies to a voter's outflow (carry + cast) volume compared to the
154      * candidate's receive volume, or to a base candidate's receive volume compared to
155      * total turnout.
156      */
157    static final byte MOSQUITO_BAR_DIVISOR = 100;
158
159
160
161    /** The common path extender for use in node views.
162      */
163    PathExtender pathExtender() { return pathExtender; }
164
165
166        private final PathExtender pathExtender;
167
168
169
170    /** The constant length of the em unit for the view's font, in pixels.  Use this
171      * constant as a pixel multiplier instead of SVG_LENGTHTYPE_EMS for general layout
172      * purposes.  This is a workaround for the surprising fact that em units for
173      * positioning in a container scale with the font size of the positioned object, not
174      * the container.  This makes it hard to consistently position objects.
175      */
176    float pxPerEm() { return pxPerEm; }
177
178
179        private float pxPerEm; // final after load
180
181
182
183    /** The spool for the release of associated holds.  When unwound it releases the holds
184      * of this view, thereby disabling it.
185      */
186    Spool spool() { return spool; }
187
188
189        private final Spool spool = new Spool1();
190
191
192
193    /** Relative y offset of sub-mnemonic text (vote volume) from the associated mnemonic
194      * text.
195      */
196    static final float SUB_MNEMONIC_DROP = 1.1f; // down below mnemonic
197
198
199
200    /** The 'svg' element of which the underlying SVG view is a descendant.
201      *
202      *     @see #svgView()
203      */
204    OMSVGSVGElement svg() { return svg; }
205
206
207
208   // - S  V  G - W r a p p e r ----------------------------------------------------------
209
210
211    public final SVGNest<?> parent() { return null; }
212
213
214
215    public final OMSVGGElement svgView() { return svgView; }
216
217
218        private final OMSVGGElement svgView = document.createSVGGElement();
219
220            { svg.appendChild( svgView ); }
221
222
223
224   // - W i d g e t ----------------------------------------------------------------------
225
226
227    protected @Override void onLoad()
228    {
229        if( !spool.isUnwinding() ) { new Loader2(); }
230        super.onLoad();
231    }
232
233
234
235    protected @Override void onUnload()
236    {
237        super.onUnload();
238        spool.unwind();
239    }
240
241
242
243   // ====================================================================================
244
245
246    /** A controller that responds to node clicks by extending or contracting the votepath.
247      */
248    final class PathExtender implements ClickHandler, PropertyChangeHandler
249    {
250
251        PathExtender()
252        {
253            spool.add( new Hold()
254            {
255                final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource(
256                  PropertyChange.TYPE, /*source*/model, PathExtender.this );
257                public void release() { hR.removeHandler(); }
258            });
259        }
260
261
262        /** Answers whether a dispatch of path extension events is currently in progress.
263          * This flag is raised when a path extension request is initiated by a node
264          * click, and lowered after the related events have cleared the votespace model.
265          */
266        boolean isPathExtending() { return isPathExtending; }
267
268            private boolean isPathExtending;
269
270
271        public void onClick( final ClickEvent e )
272        {
273            try
274            {
275                final Element sourceElement = ((OMSVGElement)e.getSource()).getElement();
276                String votepathV = sourceElement.getPropertyString( "votepath" );
277                if( votepathV == null ) return; // probably a node that is supposed to be un-displayed, and was displayed for test purposes
278
279                final String votepath = model.votepath();
280                Element prunedElement = null;
281                if( votepath.endsWith( votepathV  )) // clicked inside of current votepath
282                {
283                    prunedElement = sourceElement;
284                    votepathV = votepathV.substring( 1, votepathV.length() ); // prune back
285                }
286                else
287                {
288                    if( votepathV.length() == 1 // clicked on an end-candidate
289                      && votepath.length() > 0 ) // when another end-candidate's branch was expanded
290                    {
291                        final VoterCircle expandedCircle = centerCircle.outCircle();
292                        if( expandedCircle.isVisible() ) // true, unless there was some problem
293                        {
294                            prunedElement = expandedCircle.candidateV().localView().getElement();
295                        }
296                    }
297                    final String last = sourceElement.getPropertyString( "votepathLast" );
298                    if( last != null && last.endsWith( votepathV )) votepathV = last; // re-expand
299                }
300                if( prunedElement != null )
301                {
302                    prunedElement.setPropertyString( "votepathLast", model.votepath() );
303                }
304
305                isPathExtending = true;
306                Scenes.i().sScopingSwitch().set( DartScoping.appendSwitch( GWTX.stringBuilderClear(),
307                  model.pollName(), votepathV ).toString() );
308            }
309            catch( Exception x ) { GWTX.handleUncaughtException( x ); } // q.v. for reason
310        }
311
312
313        public void onPropertyChange( final PropertyChange e )
314        {
315            final String n = e.propertyName();
316            if( "pollName".equals(n) || "votepath".equals(n))
317            {
318                Scheduler.get().scheduleFinally( pathExtensionTerminator );
319            }
320        }
321
322
323        private Scheduler.ScheduledCommand pathExtensionTerminator = new Scheduler.ScheduledCommand()
324        {
325            public void execute() { isPathExtending = false; }
326        };
327
328
329    };
330
331
332
333//// P r i v a t e ///////////////////////////////////////////////////////////////////////
334
335
336    /** The view of the total volume of votes cast, or poll turnout.  It sits just under
337      * the poll name at midpoint of the center circle.
338      */
339    private final FlowVolumeV castVolumeV = new FlowVolumeV( VotespaceV.this );
340
341    {
342        castVolumeV.addClassNameBaseVal( "castVolumeV" );
343    }
344
345
346
347    private CenterCircle centerCircle; // final after load
348
349
350
351    /** Vertical offset of mnemonic text from its focal point.
352      */
353    private static final float POLLNAME_OFFSET_Y = -0.4f; // up
354
355
356
357    private final OMText pollNameTextNode = document.createTextNode( "" );
358
359
360
361   // ====================================================================================
362
363
364      @Warning("dead code")
365    private final class Loader implements com.google.gwt.event.dom.client.LoadHandler
366    {
367
368        Loader()
369        {
370            spool.add( new Hold()
371            {
372                final HandlerRegistration hR = svg.addLoadHandler( Loader.this );
373                public void release() { hR.removeHandler(); }
374            });
375            // SVGUnload not currently supported, so use VotespaceV.onUnload() instead.
376            // http://www.vectomatic.org/lib-gwt-svg/svg-event-mapping
377        }
378
379
380        public void onLoad( final com.google.gwt.event.dom.client.LoadEvent e )
381        {
382            // never called though SVG clearly displayed, using VotespaceV.onLoad() instead.
383            com.google.gwt.user.client.Window.alert( "loading"  );
384        }
385
386    }
387
388
389
390   // ====================================================================================
391
392
393    private final class Loader2 implements Scheduler.ScheduledCommand
394    {
395
396        Loader2()
397        {
398            standardText = document.createSVGTextElement();
399            svg.appendChild( standardText );
400            final short em = org.vectomatic.dom.svg.OMSVGLength.SVG_LENGTHTYPE_EMS;
401            standardText.getX().getBaseVal().appendItem( svg.createSVGLength( em, 1f ));
402            standardText.getY().getBaseVal().appendItem( svg.createSVGLength( em, 1f ));
403            standardText.appendChild( document.createTextNode( "standardText" ));
404
405            Scheduler.get().scheduleDeferred( Loader2.this );
406        }
407
408
409        public void execute() // after Chrome is ready to support emLength.getValue()
410        {
411          // Measure em length from standard text.
412          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
413            try
414            {
415                final OMSVGLength emLen = standardText.getX().getBaseVal().getItem( 0 );
416                pxPerEm = emLen.getValue();
417            }
418            finally{ svg.removeChild( standardText ); }
419
420          // Center per a/web/context/xf/VotespaceV.css.
421          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
422            {
423                final float xActualPx = 3000; // formal (0,0) to actual (3000,2000)
424                final float yActualPx = 2000;
425                svgView.setAttribute( "transform", "translate(" // per TRANS_ATT
426                  + xActualPx + " " + yActualPx + ")" );
427            }
428
429          // Finish constructing the view.
430          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
431            float y = 0f;
432            OMSVGTextElement text;
433
434          // poll name
435          // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
436            text = document.createSVGTextElement();
437            svgView.appendChild( text );
438            text.addClassNameBaseVal( "pollName" );
439            text.getY().getBaseVal().appendItem( svg.createSVGLength(
440              SVG_LENGTHTYPE_PX, (y += POLLNAME_OFFSET_Y) * pxPerEm ));
441            text.appendChild( pollNameTextNode );
442
443          // cast volume
444          // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
445            svgView.appendChild( castVolumeV );
446            castVolumeV.getY().getBaseVal().appendItem( svg.createSVGLength(
447              SVG_LENGTHTYPE_PX, (y += SUB_MNEMONIC_DROP) * pxPerEm ));
448
449          // center circle
450          // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
451            centerCircle = new CenterCircle(VotespaceV.this);
452            centerCircle.setParent( VotespaceV.this );
453        }
454
455
456        private final OMSVGTextElement standardText;
457    }
458
459
460
461   // ====================================================================================
462
463
464    private final class Modeller implements PropertyChangeHandler, SelectionChangeEvent.Handler
465    {
466
467        Modeller()
468        {
469            spool.add( new Hold()
470            {
471                final HandlerRegistration hR = model.addSelectionChangeHandler(
472                  Modeller.this );
473                public void release() { hR.removeHandler(); }
474            });
475            spool.add( new Hold()
476            {
477                final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource(
478                  PropertyChange.TYPE, /*source*/model, Modeller.this );
479                public void release() { hR.removeHandler(); }
480            });
481            remodelPollName(); // init state
482            remodelSac();
483        }
484
485
486        private final @Warning("init call") void remodelPollName()
487        {
488            final String pollName = model().pollName();
489            if( pollName == null )
490            {
491                pollNameTextNode.setData(
492                  App.i().mesS().gwt_scene_vote_VotespaceV_pollNameUnspecified() );
493            }
494            else pollNameTextNode.setData( pollName );
495        }
496
497
498        private final @Warning("init call") void remodelSac()
499        {
500            final SacJS sac = model.getSac();
501            String newBodyClass = "countingMethods-";
502            if( sac == null )
503            {
504                assert model().count() == null;
505                castVolumeV.setPlaceholder(
506                  App.i().mesS().gwt_scene_vote_VotespaceV_countUnknown() );
507            }
508            else
509            {
510                final CountingMethodJS.SwitchMnemonic mCM = sac.countingMethodMnemonic();
511                newBodyClass += mCM.name();
512                if( mCM == CountingMethodJS.SwitchMnemonic.q )
513                {
514                    final SacJS_q qSac = sac.cast();
515                    castVolumeV.set( qSac.castVolume() );
516                }
517                else if( mCM == CountingMethodJS.SwitchMnemonic.v )
518                {
519                    final SacJS_v vSac = sac.cast();
520                    castVolumeV.set( vSac.castVolume() );
521                }
522            }
523            setBodyClass( newBodyClass );
524        }
525
526
527        private final @Warning("init call") void setBodyClass( final String newBodyClass )
528        {
529            if( newBodyClass.equals( bodyClass )) return;
530
531            final Element body = RootPanel.getBodyElement();
532            if( bodyClass != null ) body.removeClassName( bodyClass ); // null on init only
533
534            bodyClass = newBodyClass;
535            body.addClassName( bodyClass );
536        }
537
538            private String bodyClass;
539
540
541       // - P r o p e r t y - C h a n g e - H a n d l e r --------------------------------
542
543
544        public void onPropertyChange( final PropertyChange e )
545        {
546            if( "pollName".equals( e.propertyName() )) remodelPollName();
547        }
548
549
550       // - S e l e c t i o n - C h a n g e - E v e n t . H a n d l e r ------------------
551
552
553        public void onSelectionChange( SelectionChangeEvent _e ) { remodelSac(); }
554
555    }
556
557
558
559}