001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * -------------
028 * DialPlot.java
029 * -------------
030 * (C) Copyright 2006-2008, by Object Refinery Limited.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   -;
034 *
035 * Changes
036 * -------
037 * 03-Nov-2006 : Version 1 (DG);
038 * 08-Mar-2007 : Fix in hashCode() (DG);
039 * 17-Oct-2007 : Fixed listener registration/deregistration bugs (DG);
040 * 24-Oct-2007 : Maintain pointers in their own list, so they can be
041 *               drawn after other layers (DG);
042 * 15-Feb-2007 : Fixed clipping bug (1873160) (DG);
043 *
044 */
045
046package org.jfree.chart.plot.dial;
047
048import java.awt.Graphics2D;
049import java.awt.Shape;
050import java.awt.geom.Point2D;
051import java.awt.geom.Rectangle2D;
052import java.io.IOException;
053import java.io.ObjectInputStream;
054import java.io.ObjectOutputStream;
055import java.util.Iterator;
056import java.util.List;
057
058import org.jfree.chart.JFreeChart;
059import org.jfree.chart.event.PlotChangeEvent;
060import org.jfree.chart.plot.Plot;
061import org.jfree.chart.plot.PlotRenderingInfo;
062import org.jfree.chart.plot.PlotState;
063import org.jfree.data.general.DatasetChangeEvent;
064import org.jfree.data.general.ValueDataset;
065import org.jfree.util.ObjectList;
066import org.jfree.util.ObjectUtilities;
067
068/**
069 * A dial plot composed of user-definable layers.
070 * The example shown here is generated by the <code>DialDemo2.java</code>
071 * program included in the JFreeChart Demo Collection:
072 * <br><br>
073 * <img src="../../../../../images/DialPlotSample.png"
074 * alt="DialPlotSample.png" />
075 *
076 * @since 1.0.7
077 */
078public class DialPlot extends Plot implements DialLayerChangeListener {
079
080    /**
081     * The background layer (optional).
082     */
083    private DialLayer background;
084
085    /**
086     * The needle cap (optional).
087     */
088    private DialLayer cap;
089
090    /**
091     * The dial frame.
092     */
093    private DialFrame dialFrame;
094
095    /**
096     * The dataset(s) for the dial plot.
097     */
098    private ObjectList datasets;
099
100    /**
101     * The scale(s) for the dial plot.
102     */
103    private ObjectList scales;
104
105    /** Storage for keys that map datasets to scales. */
106    private ObjectList datasetToScaleMap;
107
108    /**
109     * The drawing layers for the dial plot.
110     */
111    private List layers;
112
113    /**
114     * The pointer(s) for the dial.
115     */
116    private List pointers;
117
118    /**
119     * The x-coordinate for the view window.
120     */
121    private double viewX;
122
123    /**
124     * The y-coordinate for the view window.
125     */
126    private double viewY;
127
128    /**
129     * The width of the view window, expressed as a percentage.
130     */
131    private double viewW;
132
133    /**
134     * The height of the view window, expressed as a percentage.
135     */
136    private double viewH;
137
138    /**
139     * Creates a new instance of <code>DialPlot</code>.
140     */
141    public DialPlot() {
142        this(null);
143    }
144
145    /**
146     * Creates a new instance of <code>DialPlot</code>.
147     *
148     * @param dataset  the dataset (<code>null</code> permitted).
149     */
150    public DialPlot(ValueDataset dataset) {
151        this.background = null;
152        this.cap = null;
153        this.dialFrame = new ArcDialFrame();
154        this.datasets = new ObjectList();
155        if (dataset != null) {
156            setDataset(dataset);
157        }
158        this.scales = new ObjectList();
159        this.datasetToScaleMap = new ObjectList();
160        this.layers = new java.util.ArrayList();
161        this.pointers = new java.util.ArrayList();
162        this.viewX = 0.0;
163        this.viewY = 0.0;
164        this.viewW = 1.0;
165        this.viewH = 1.0;
166    }
167
168    /**
169     * Returns the background.
170     *
171     * @return The background (possibly <code>null</code>).
172     *
173     * @see #setBackground(DialLayer)
174     */
175    public DialLayer getBackground() {
176        return this.background;
177    }
178
179    /**
180     * Sets the background layer and sends a {@link PlotChangeEvent} to all
181     * registered listeners.
182     *
183     * @param background  the background layer (<code>null</code> permitted).
184     *
185     * @see #getBackground()
186     */
187    public void setBackground(DialLayer background) {
188        if (this.background != null) {
189            this.background.removeChangeListener(this);
190        }
191        this.background = background;
192        if (background != null) {
193            background.addChangeListener(this);
194        }
195        fireChangeEvent();
196    }
197
198    /**
199     * Returns the cap.
200     *
201     * @return The cap (possibly <code>null</code>).
202     *
203     * @see #setCap(DialLayer)
204     */
205    public DialLayer getCap() {
206        return this.cap;
207    }
208
209    /**
210     * Sets the cap and sends a {@link PlotChangeEvent} to all registered
211     * listeners.
212     *
213     * @param cap  the cap (<code>null</code> permitted).
214     *
215     * @see #getCap()
216     */
217    public void setCap(DialLayer cap) {
218        if (this.cap != null) {
219            this.cap.removeChangeListener(this);
220        }
221        this.cap = cap;
222        if (cap != null) {
223            cap.addChangeListener(this);
224        }
225        fireChangeEvent();
226    }
227
228    /**
229     * Returns the dial's frame.
230     *
231     * @return The dial's frame (never <code>null</code>).
232     *
233     * @see #setDialFrame(DialFrame)
234     */
235    public DialFrame getDialFrame() {
236        return this.dialFrame;
237    }
238
239    /**
240     * Sets the dial's frame and sends a {@link PlotChangeEvent} to all
241     * registered listeners.
242     *
243     * @param frame  the frame (<code>null</code> not permitted).
244     *
245     * @see #getDialFrame()
246     */
247    public void setDialFrame(DialFrame frame) {
248        if (frame == null) {
249            throw new IllegalArgumentException("Null 'frame' argument.");
250        }
251        this.dialFrame.removeChangeListener(this);
252        this.dialFrame = frame;
253        frame.addChangeListener(this);
254        fireChangeEvent();
255    }
256
257    /**
258     * Returns the x-coordinate of the viewing rectangle.  This is specified
259     * in the range 0.0 to 1.0, relative to the dial's framing rectangle.
260     *
261     * @return The x-coordinate of the viewing rectangle.
262     *
263     * @see #setView(double, double, double, double)
264     */
265    public double getViewX() {
266        return this.viewX;
267    }
268
269    /**
270     * Returns the y-coordinate of the viewing rectangle.  This is specified
271     * in the range 0.0 to 1.0, relative to the dial's framing rectangle.
272     *
273     * @return The y-coordinate of the viewing rectangle.
274     *
275     * @see #setView(double, double, double, double)
276     */
277    public double getViewY() {
278        return this.viewY;
279    }
280
281    /**
282     * Returns the width of the viewing rectangle.  This is specified
283     * in the range 0.0 to 1.0, relative to the dial's framing rectangle.
284     *
285     * @return The width of the viewing rectangle.
286     *
287     * @see #setView(double, double, double, double)
288     */
289    public double getViewWidth() {
290        return this.viewW;
291    }
292
293    /**
294     * Returns the height of the viewing rectangle.  This is specified
295     * in the range 0.0 to 1.0, relative to the dial's framing rectangle.
296     *
297     * @return The height of the viewing rectangle.
298     *
299     * @see #setView(double, double, double, double)
300     */
301    public double getViewHeight() {
302        return this.viewH;
303    }
304
305    /**
306     * Sets the viewing rectangle, relative to the dial's framing rectangle,
307     * and sends a {@link PlotChangeEvent} to all registered listeners.
308     *
309     * @param x  the x-coordinate (in the range 0.0 to 1.0).
310     * @param y  the y-coordinate (in the range 0.0 to 1.0).
311     * @param w  the width (in the range 0.0 to 1.0).
312     * @param h  the height (in the range 0.0 to 1.0).
313     *
314     * @see #getViewX()
315     * @see #getViewY()
316     * @see #getViewWidth()
317     * @see #getViewHeight()
318     */
319    public void setView(double x, double y, double w, double h) {
320        this.viewX = x;
321        this.viewY = y;
322        this.viewW = w;
323        this.viewH = h;
324        fireChangeEvent();
325    }
326
327    /**
328     * Adds a layer to the plot and sends a {@link PlotChangeEvent} to all
329     * registered listeners.
330     *
331     * @param layer  the layer (<code>null</code> not permitted).
332     */
333    public void addLayer(DialLayer layer) {
334        if (layer == null) {
335            throw new IllegalArgumentException("Null 'layer' argument.");
336        }
337        this.layers.add(layer);
338        layer.addChangeListener(this);
339        fireChangeEvent();
340    }
341
342    /**
343     * Returns the index for the specified layer.
344     *
345     * @param layer  the layer (<code>null</code> not permitted).
346     *
347     * @return The layer index.
348     */
349    public int getLayerIndex(DialLayer layer) {
350        if (layer == null) {
351            throw new IllegalArgumentException("Null 'layer' argument.");
352        }
353        return this.layers.indexOf(layer);
354    }
355
356    /**
357     * Removes the layer at the specified index and sends a
358     * {@link PlotChangeEvent} to all registered listeners.
359     *
360     * @param index  the index.
361     */
362    public void removeLayer(int index) {
363        DialLayer layer = (DialLayer) this.layers.get(index);
364        if (layer != null) {
365            layer.removeChangeListener(this);
366        }
367        this.layers.remove(index);
368        fireChangeEvent();
369    }
370
371    /**
372     * Removes the specified layer and sends a {@link PlotChangeEvent} to all
373     * registered listeners.
374     *
375     * @param layer  the layer (<code>null</code> not permitted).
376     */
377    public void removeLayer(DialLayer layer) {
378        // defer argument checking
379        removeLayer(getLayerIndex(layer));
380    }
381
382    /**
383     * Adds a pointer to the plot and sends a {@link PlotChangeEvent} to all
384     * registered listeners.
385     *
386     * @param pointer  the pointer (<code>null</code> not permitted).
387     */
388    public void addPointer(DialPointer pointer) {
389        if (pointer == null) {
390            throw new IllegalArgumentException("Null 'pointer' argument.");
391        }
392        this.pointers.add(pointer);
393        pointer.addChangeListener(this);
394        fireChangeEvent();
395    }
396
397    /**
398     * Returns the index for the specified pointer.
399     *
400     * @param pointer  the pointer (<code>null</code> not permitted).
401     *
402     * @return The pointer index.
403     */
404    public int getPointerIndex(DialPointer pointer) {
405        if (pointer == null) {
406            throw new IllegalArgumentException("Null 'pointer' argument.");
407        }
408        return this.pointers.indexOf(pointer);
409    }
410
411    /**
412     * Removes the pointer at the specified index and sends a
413     * {@link PlotChangeEvent} to all registered listeners.
414     *
415     * @param index  the index.
416     */
417    public void removePointer(int index) {
418        DialPointer pointer = (DialPointer) this.pointers.get(index);
419        if (pointer != null) {
420            pointer.removeChangeListener(this);
421        }
422        this.pointers.remove(index);
423        fireChangeEvent();
424    }
425
426    /**
427     * Removes the specified pointer and sends a {@link PlotChangeEvent} to all
428     * registered listeners.
429     *
430     * @param pointer  the pointer (<code>null</code> not permitted).
431     */
432    public void removePointer(DialPointer pointer) {
433        // defer argument checking
434        removeLayer(getPointerIndex(pointer));
435    }
436
437    /**
438     * Returns the dial pointer that is associated with the specified
439     * dataset, or <code>null</code>.
440     *
441     * @param datasetIndex  the dataset index.
442     *
443     * @return The pointer.
444     */
445    public DialPointer getPointerForDataset(int datasetIndex) {
446        DialPointer result = null;
447        Iterator iterator = this.pointers.iterator();
448        while (iterator.hasNext()) {
449            DialPointer p = (DialPointer) iterator.next();
450            if (p.getDatasetIndex() == datasetIndex) {
451                return p;
452            }
453        }
454        return result;
455    }
456
457    /**
458     * Returns the primary dataset for the plot.
459     *
460     * @return The primary dataset (possibly <code>null</code>).
461     */
462    public ValueDataset getDataset() {
463        return getDataset(0);
464    }
465
466    /**
467     * Returns the dataset at the given index.
468     *
469     * @param index  the dataset index.
470     *
471     * @return The dataset (possibly <code>null</code>).
472     */
473    public ValueDataset getDataset(int index) {
474        ValueDataset result = null;
475        if (this.datasets.size() > index) {
476            result = (ValueDataset) this.datasets.get(index);
477        }
478        return result;
479    }
480
481    /**
482     * Sets the dataset for the plot, replacing the existing dataset, if there
483     * is one, and sends a {@link PlotChangeEvent} to all registered
484     * listeners.
485     *
486     * @param dataset  the dataset (<code>null</code> permitted).
487     */
488    public void setDataset(ValueDataset dataset) {
489        setDataset(0, dataset);
490    }
491
492    /**
493     * Sets a dataset for the plot.
494     *
495     * @param index  the dataset index.
496     * @param dataset  the dataset (<code>null</code> permitted).
497     */
498    public void setDataset(int index, ValueDataset dataset) {
499
500        ValueDataset existing = (ValueDataset) this.datasets.get(index);
501        if (existing != null) {
502            existing.removeChangeListener(this);
503        }
504        this.datasets.set(index, dataset);
505        if (dataset != null) {
506            dataset.addChangeListener(this);
507        }
508
509        // send a dataset change event to self...
510        DatasetChangeEvent event = new DatasetChangeEvent(this, dataset);
511        datasetChanged(event);
512
513    }
514
515    /**
516     * Returns the number of datasets.
517     *
518     * @return The number of datasets.
519     */
520    public int getDatasetCount() {
521        return this.datasets.size();
522    }
523
524    /**
525     * Draws the plot.  This method is usually called by the {@link JFreeChart}
526     * instance that manages the plot.
527     *
528     * @param g2  the graphics target.
529     * @param area  the area in which the plot should be drawn.
530     * @param anchor  the anchor point (typically the last point that the
531     *     mouse clicked on, <code>null</code> is permitted).
532     * @param parentState  the state for the parent plot (if any).
533     * @param info  used to collect plot rendering info (<code>null</code>
534     *     permitted).
535     */
536    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
537            PlotState parentState, PlotRenderingInfo info) {
538
539        Shape origClip = g2.getClip();
540        g2.setClip(area);
541
542        // first, expand the viewing area into a drawing frame
543        Rectangle2D frame = viewToFrame(area);
544
545        // draw the background if there is one...
546        if (this.background != null && this.background.isVisible()) {
547            if (this.background.isClippedToWindow()) {
548                Shape savedClip = g2.getClip();
549                g2.clip(this.dialFrame.getWindow(frame));
550                this.background.draw(g2, this, frame, area);
551                g2.setClip(savedClip);
552            }
553            else {
554                this.background.draw(g2, this, frame, area);
555            }
556        }
557
558        Iterator iterator = this.layers.iterator();
559        while (iterator.hasNext()) {
560            DialLayer current = (DialLayer) iterator.next();
561            if (current.isVisible()) {
562                if (current.isClippedToWindow()) {
563                    Shape savedClip = g2.getClip();
564                    g2.clip(this.dialFrame.getWindow(frame));
565                    current.draw(g2, this, frame, area);
566                    g2.setClip(savedClip);
567                }
568                else {
569                    current.draw(g2, this, frame, area);
570                }
571            }
572        }
573
574        // draw the pointers
575        iterator = this.pointers.iterator();
576        while (iterator.hasNext()) {
577            DialPointer current = (DialPointer) iterator.next();
578            if (current.isVisible()) {
579                if (current.isClippedToWindow()) {
580                    Shape savedClip = g2.getClip();
581                    g2.clip(this.dialFrame.getWindow(frame));
582                    current.draw(g2, this, frame, area);
583                    g2.setClip(savedClip);
584                }
585                else {
586                    current.draw(g2, this, frame, area);
587                }
588            }
589        }
590
591        // draw the cap if there is one...
592        if (this.cap != null && this.cap.isVisible()) {
593            if (this.cap.isClippedToWindow()) {
594                Shape savedClip = g2.getClip();
595                g2.clip(this.dialFrame.getWindow(frame));
596                this.cap.draw(g2, this, frame, area);
597                g2.setClip(savedClip);
598            }
599            else {
600                this.cap.draw(g2, this, frame, area);
601            }
602        }
603
604        if (this.dialFrame.isVisible()) {
605            this.dialFrame.draw(g2, this, frame, area);
606        }
607
608        g2.setClip(origClip);
609
610    }
611
612    /**
613     * Returns the frame surrounding the specified view rectangle.
614     *
615     * @param view  the view rectangle (<code>null</code> not permitted).
616     *
617     * @return The frame rectangle.
618     */
619    private Rectangle2D viewToFrame(Rectangle2D view) {
620        double width = view.getWidth() / this.viewW;
621        double height = view.getHeight() / this.viewH;
622        double x = view.getX() - (width * this.viewX);
623        double y = view.getY() - (height * this.viewY);
624        return new Rectangle2D.Double(x, y, width, height);
625    }
626
627    /**
628     * Returns the value from the specified dataset.
629     *
630     * @param datasetIndex  the dataset index.
631     *
632     * @return The data value.
633     */
634    public double getValue(int datasetIndex) {
635        double result = Double.NaN;
636        ValueDataset dataset = getDataset(datasetIndex);
637        if (dataset != null) {
638            Number n = dataset.getValue();
639            if (n != null) {
640                result = n.doubleValue();
641            }
642        }
643        return result;
644    }
645
646    /**
647     * Adds a dial scale to the plot and sends a {@link PlotChangeEvent} to
648     * all registered listeners.
649     *
650     * @param index  the scale index.
651     * @param scale  the scale (<code>null</code> not permitted).
652     */
653    public void addScale(int index, DialScale scale) {
654        if (scale == null) {
655            throw new IllegalArgumentException("Null 'scale' argument.");
656        }
657        DialScale existing = (DialScale) this.scales.get(index);
658        if (existing != null) {
659            removeLayer(existing);
660        }
661        this.layers.add(scale);
662        this.scales.set(index, scale);
663        scale.addChangeListener(this);
664        fireChangeEvent();
665    }
666
667    /**
668     * Returns the scale at the given index.
669     *
670     * @param index  the scale index.
671     *
672     * @return The scale (possibly <code>null</code>).
673     */
674    public DialScale getScale(int index) {
675        DialScale result = null;
676        if (this.scales.size() > index) {
677            result = (DialScale) this.scales.get(index);
678        }
679        return result;
680    }
681
682    /**
683     * Maps a dataset to a particular scale.
684     *
685     * @param index  the dataset index (zero-based).
686     * @param scaleIndex  the scale index (zero-based).
687     */
688    public void mapDatasetToScale(int index, int scaleIndex) {
689        this.datasetToScaleMap.set(index, new Integer(scaleIndex));
690        fireChangeEvent();
691    }
692
693    /**
694     * Returns the dial scale for a specific dataset.
695     *
696     * @param datasetIndex  the dataset index.
697     *
698     * @return The dial scale.
699     */
700    public DialScale getScaleForDataset(int datasetIndex) {
701        DialScale result = (DialScale) this.scales.get(0);
702        Integer scaleIndex = (Integer) this.datasetToScaleMap.get(datasetIndex);
703        if (scaleIndex != null) {
704            result = getScale(scaleIndex.intValue());
705        }
706        return result;
707    }
708
709    /**
710     * A utility method that computes a rectangle using relative radius values.
711     *
712     * @param rect  the reference rectangle (<code>null</code> not permitted).
713     * @param radiusW  the width radius (must be > 0.0)
714     * @param radiusH  the height radius.
715     *
716     * @return A new rectangle.
717     */
718    public static Rectangle2D rectangleByRadius(Rectangle2D rect,
719            double radiusW, double radiusH) {
720        if (rect == null) {
721            throw new IllegalArgumentException("Null 'rect' argument.");
722        }
723        double x = rect.getCenterX();
724        double y = rect.getCenterY();
725        double w = rect.getWidth() * radiusW;
726        double h = rect.getHeight() * radiusH;
727        return new Rectangle2D.Double(x - w / 2.0, y - h / 2.0, w, h);
728    }
729
730    /**
731     * Receives notification when a layer has changed, and responds by
732     * forwarding a {@link PlotChangeEvent} to all registered listeners.
733     *
734     * @param event  the event.
735     */
736    public void dialLayerChanged(DialLayerChangeEvent event) {
737        fireChangeEvent();
738    }
739
740    /**
741     * Tests this <code>DialPlot</code> instance for equality with an
742     * arbitrary object.  The plot's dataset(s) is (are) not included in
743     * the test.
744     *
745     * @param obj  the object (<code>null</code> permitted).
746     *
747     * @return A boolean.
748     */
749    public boolean equals(Object obj) {
750        if (obj == this) {
751            return true;
752        }
753        if (!(obj instanceof DialPlot)) {
754            return false;
755        }
756        DialPlot that = (DialPlot) obj;
757        if (!ObjectUtilities.equal(this.background, that.background)) {
758            return false;
759        }
760        if (!ObjectUtilities.equal(this.cap, that.cap)) {
761            return false;
762        }
763        if (!this.dialFrame.equals(that.dialFrame)) {
764            return false;
765        }
766        if (this.viewX != that.viewX) {
767            return false;
768        }
769        if (this.viewY != that.viewY) {
770            return false;
771        }
772        if (this.viewW != that.viewW) {
773            return false;
774        }
775        if (this.viewH != that.viewH) {
776            return false;
777        }
778        if (!this.layers.equals(that.layers)) {
779            return false;
780        }
781        if (!this.pointers.equals(that.pointers)) {
782            return false;
783        }
784        return super.equals(obj);
785    }
786
787    /**
788     * Returns a hash code for this instance.
789     *
790     * @return The hash code.
791     */
792    public int hashCode() {
793        int result = 193;
794        result = 37 * result + ObjectUtilities.hashCode(this.background);
795        result = 37 * result + ObjectUtilities.hashCode(this.cap);
796        result = 37 * result + this.dialFrame.hashCode();
797        long temp = Double.doubleToLongBits(this.viewX);
798        result = 37 * result + (int) (temp ^ (temp >>> 32));
799        temp = Double.doubleToLongBits(this.viewY);
800        result = 37 * result + (int) (temp ^ (temp >>> 32));
801        temp = Double.doubleToLongBits(this.viewW);
802        result = 37 * result + (int) (temp ^ (temp >>> 32));
803        temp = Double.doubleToLongBits(this.viewH);
804        result = 37 * result + (int) (temp ^ (temp >>> 32));
805        return result;
806    }
807
808    /**
809     * Returns the plot type.
810     *
811     * @return <code>"DialPlot"</code>
812     */
813    public String getPlotType() {
814        return "DialPlot";
815    }
816
817    /**
818     * Provides serialization support.
819     *
820     * @param stream  the output stream.
821     *
822     * @throws IOException  if there is an I/O error.
823     */
824    private void writeObject(ObjectOutputStream stream) throws IOException {
825        stream.defaultWriteObject();
826    }
827
828    /**
829     * Provides serialization support.
830     *
831     * @param stream  the input stream.
832     *
833     * @throws IOException  if there is an I/O error.
834     * @throws ClassNotFoundException  if there is a classpath problem.
835     */
836    private void readObject(ObjectInputStream stream)
837            throws IOException, ClassNotFoundException {
838        stream.defaultReadObject();
839    }
840
841
842}