001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ------------------
028 * AbstractBlock.java
029 * ------------------
030 * (C) Copyright 2004-present, by David Gilbert.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Tracy Hiltbrand (equals/hashCode comply with EqualsVerifier);
034 *
035 */
036
037package org.jfree.chart.block;
038
039import java.awt.Graphics2D;
040import java.awt.geom.Rectangle2D;
041import java.io.IOException;
042import java.io.ObjectInputStream;
043import java.io.ObjectOutputStream;
044import java.io.Serializable;
045import java.util.Objects;
046import org.jfree.chart.ui.RectangleInsets;
047import org.jfree.chart.ui.Size2D;
048import org.jfree.chart.util.Args;
049import org.jfree.chart.util.PublicCloneable;
050import org.jfree.chart.util.SerialUtils;
051import org.jfree.chart.util.ShapeUtils;
052
053import org.jfree.data.Range;
054
055/**
056 * A convenience class for creating new classes that implement
057 * the {@link Block} interface.
058 */
059public class AbstractBlock implements Cloneable, Serializable {
060
061    /** For serialization. */
062    private static final long serialVersionUID = 7689852412141274563L;
063
064    /** The id for the block. */
065    private String id;
066
067    /** The margin around the outside of the block. */
068    private RectangleInsets margin;
069
070    /** The frame (or border) for the block. */
071    private BlockFrame frame;
072
073    /** The padding between the block content and the border. */
074    private RectangleInsets padding;
075
076    /**
077     * The natural width of the block (may be overridden if there are
078     * constraints in sizing).
079     */
080    private double width;
081
082    /**
083     * The natural height of the block (may be overridden if there are
084     * constraints in sizing).
085     */
086    private double height;
087
088    /**
089     * The current bounds for the block (position of the block in Java2D space).
090     */
091    private transient Rectangle2D bounds;
092
093    /**
094     * Creates a new block.
095     */
096    protected AbstractBlock() {
097        this.id = null;
098        this.width = 0.0;
099        this.height = 0.0;
100        this.bounds = new Rectangle2D.Float();
101        this.margin = RectangleInsets.ZERO_INSETS;
102        this.frame = BlockBorder.NONE;
103        this.padding = RectangleInsets.ZERO_INSETS;
104    }
105
106    /**
107     * Returns the id.
108     *
109     * @return The id (possibly {@code null}).
110     *
111     * @see #setID(String)
112     */
113    public String getID() {
114        return this.id;
115    }
116
117    /**
118     * Sets the id for the block.
119     *
120     * @param id  the id ({@code null} permitted).
121     *
122     * @see #getID()
123     */
124    public void setID(String id) {
125        this.id = id;
126    }
127
128    /**
129     * Returns the natural width of the block, if this is known in advance.
130     * The actual width of the block may be overridden if layout constraints
131     * make this necessary.
132     *
133     * @return The width.
134     *
135     * @see #setWidth(double)
136     */
137    public double getWidth() {
138        return this.width;
139    }
140
141    /**
142     * Sets the natural width of the block, if this is known in advance.
143     *
144     * @param width  the width (in Java2D units)
145     *
146     * @see #getWidth()
147     */
148    public void setWidth(double width) {
149        this.width = width;
150    }
151
152    /**
153     * Returns the natural height of the block, if this is known in advance.
154     * The actual height of the block may be overridden if layout constraints
155     * make this necessary.
156     *
157     * @return The height.
158     *
159     * @see #setHeight(double)
160     */
161    public double getHeight() {
162        return this.height;
163    }
164
165    /**
166     * Sets the natural width of the block, if this is known in advance.
167     *
168     * @param height  the width (in Java2D units)
169     *
170     * @see #getHeight()
171     */
172    public void setHeight(double height) {
173        this.height = height;
174    }
175
176    /**
177     * Returns the margin.
178     *
179     * @return The margin (never {@code null}).
180     *
181     * @see #getMargin()
182     */
183    public RectangleInsets getMargin() {
184        return this.margin;
185    }
186
187    /**
188     * Sets the margin (use {@link RectangleInsets#ZERO_INSETS} for no
189     * padding).
190     *
191     * @param margin  the margin ({@code null} not permitted).
192     *
193     * @see #getMargin()
194     */
195    public void setMargin(RectangleInsets margin) {
196        Args.nullNotPermitted(margin, "margin");
197        this.margin = margin;
198    }
199
200    /**
201     * Sets the margin.
202     *
203     * @param top  the top margin.
204     * @param left  the left margin.
205     * @param bottom  the bottom margin.
206     * @param right  the right margin.
207     *
208     * @see #getMargin()
209     */
210    public void setMargin(double top, double left, double bottom, 
211            double right) {
212        setMargin(new RectangleInsets(top, left, bottom, right));
213    }
214
215    /**
216     * Sets a black border with the specified line widths.
217     *
218     * @param top  the top border line width.
219     * @param left  the left border line width.
220     * @param bottom  the bottom border line width.
221     * @param right  the right border line width.
222     */
223    public void setBorder(double top, double left, double bottom,
224                          double right) {
225        setFrame(new BlockBorder(top, left, bottom, right));
226    }
227
228    /**
229     * Returns the current frame (border).
230     *
231     * @return The frame.
232     *
233     * @see #setFrame(BlockFrame)
234     */
235    public BlockFrame getFrame() {
236        return this.frame;
237    }
238
239    /**
240     * Sets the frame (or border).
241     *
242     * @param frame  the frame ({@code null} not permitted).
243     *
244     * @see #getFrame()
245     */
246    public void setFrame(BlockFrame frame) {
247        Args.nullNotPermitted(frame, "frame");
248        this.frame = frame;
249    }
250
251    /**
252     * Returns the padding.
253     *
254     * @return The padding (never {@code null}).
255     *
256     * @see #setPadding(RectangleInsets)
257     */
258    public RectangleInsets getPadding() {
259        return this.padding;
260    }
261
262    /**
263     * Sets the padding (use {@link RectangleInsets#ZERO_INSETS} for no
264     * padding).
265     *
266     * @param padding  the padding ({@code null} not permitted).
267     *
268     * @see #getPadding()
269     */
270    public void setPadding(RectangleInsets padding) {
271        Args.nullNotPermitted(padding, "padding");
272        this.padding = padding;
273    }
274
275    /**
276     * Sets the padding.
277     *
278     * @param top  the top padding.
279     * @param left  the left padding.
280     * @param bottom  the bottom padding.
281     * @param right  the right padding.
282     */
283    public void setPadding(double top, double left, double bottom,
284                           double right) {
285        setPadding(new RectangleInsets(top, left, bottom, right));
286    }
287
288    /**
289     * Returns the x-offset for the content within the block.
290     *
291     * @return The x-offset.
292     *
293     * @see #getContentYOffset()
294     */
295    public double getContentXOffset() {
296        return this.margin.getLeft() + this.frame.getInsets().getLeft()
297            + this.padding.getLeft();
298    }
299
300    /**
301     * Returns the y-offset for the content within the block.
302     *
303     * @return The y-offset.
304     *
305     * @see #getContentXOffset()
306     */
307    public double getContentYOffset() {
308        return this.margin.getTop() + this.frame.getInsets().getTop()
309            + this.padding.getTop();
310    }
311
312    /**
313     * Arranges the contents of the block, with no constraints, and returns
314     * the block size.
315     *
316     * @param g2  the graphics device.
317     *
318     * @return The block size (in Java2D units, never {@code null}).
319     */
320    public Size2D arrange(Graphics2D g2) {
321        return arrange(g2, RectangleConstraint.NONE);
322    }
323
324    /**
325     * Arranges the contents of the block, within the given constraints, and
326     * returns the block size.
327     *
328     * @param g2  the graphics device.
329     * @param constraint  the constraint ({@code null} not permitted).
330     *
331     * @return The block size (in Java2D units, never {@code null}).
332     */
333    public Size2D arrange(Graphics2D g2, RectangleConstraint constraint) {
334        Size2D base = new Size2D(getWidth(), getHeight());
335        return constraint.calculateConstrainedSize(base);
336    }
337
338    /**
339     * Returns the current bounds of the block.
340     *
341     * @return The bounds.
342     *
343     * @see #setBounds(Rectangle2D)
344     */
345    public Rectangle2D getBounds() {
346        return this.bounds;
347    }
348
349    /**
350     * Sets the bounds of the block.
351     *
352     * @param bounds  the bounds ({@code null} not permitted).
353     *
354     * @see #getBounds()
355     */
356    public void setBounds(Rectangle2D bounds) {
357        Args.nullNotPermitted(bounds, "bounds");
358        this.bounds = bounds;
359    }
360
361    /**
362     * Calculate the width available for content after subtracting
363     * the margin, border and padding space from the specified fixed
364     * width.
365     *
366     * @param fixedWidth  the fixed width.
367     *
368     * @return The available space.
369     *
370     * @see #trimToContentHeight(double)
371     */
372    protected double trimToContentWidth(double fixedWidth) {
373        double result = this.margin.trimWidth(fixedWidth);
374        result = this.frame.getInsets().trimWidth(result);
375        result = this.padding.trimWidth(result);
376        return Math.max(result, 0.0);
377    }
378
379    /**
380     * Calculate the height available for content after subtracting
381     * the margin, border and padding space from the specified fixed
382     * height.
383     *
384     * @param fixedHeight  the fixed height.
385     *
386     * @return The available space.
387     *
388     * @see #trimToContentWidth(double)
389     */
390    protected double trimToContentHeight(double fixedHeight) {
391        double result = this.margin.trimHeight(fixedHeight);
392        result = this.frame.getInsets().trimHeight(result);
393        result = this.padding.trimHeight(result);
394        return Math.max(result, 0.0);
395    }
396
397    /**
398     * Returns a constraint for the content of this block that will result in
399     * the bounds of the block matching the specified constraint.
400     *
401     * @param c  the outer constraint ({@code null} not permitted).
402     *
403     * @return The content constraint.
404     */
405    protected RectangleConstraint toContentConstraint(RectangleConstraint c) {
406        Args.nullNotPermitted(c, "c");
407        if (c.equals(RectangleConstraint.NONE)) {
408            return c;
409        }
410        double w = c.getWidth();
411        Range wr = c.getWidthRange();
412        double h = c.getHeight();
413        Range hr = c.getHeightRange();
414        double ww = trimToContentWidth(w);
415        double hh = trimToContentHeight(h);
416        Range wwr = trimToContentWidth(wr);
417        Range hhr = trimToContentHeight(hr);
418        return new RectangleConstraint(ww, wwr, c.getWidthConstraintType(),
419            hh, hhr, c.getHeightConstraintType());
420    }
421
422    private Range trimToContentWidth(Range r) {
423        if (r == null) {
424            return null;
425        }
426        double lowerBound = 0.0;
427        double upperBound = Double.POSITIVE_INFINITY;
428        if (r.getLowerBound() > 0.0) {
429            lowerBound = trimToContentWidth(r.getLowerBound());
430        }
431        if (r.getUpperBound() < Double.POSITIVE_INFINITY) {
432            upperBound = trimToContentWidth(r.getUpperBound());
433        }
434        return new Range(lowerBound, upperBound);
435    }
436
437    private Range trimToContentHeight(Range r) {
438        if (r == null) {
439            return null;
440        }
441        double lowerBound = 0.0;
442        double upperBound = Double.POSITIVE_INFINITY;
443        if (r.getLowerBound() > 0.0) {
444            lowerBound = trimToContentHeight(r.getLowerBound());
445        }
446        if (r.getUpperBound() < Double.POSITIVE_INFINITY) {
447            upperBound = trimToContentHeight(r.getUpperBound());
448        }
449        return new Range(lowerBound, upperBound);
450    }
451
452    /**
453     * Adds the margin, border and padding to the specified content width.
454     *
455     * @param contentWidth  the content width.
456     *
457     * @return The adjusted width.
458     */
459    protected double calculateTotalWidth(double contentWidth) {
460        double result = contentWidth;
461        result = this.padding.extendWidth(result);
462        result = this.frame.getInsets().extendWidth(result);
463        result = this.margin.extendWidth(result);
464        return result;
465    }
466
467    /**
468     * Adds the margin, border and padding to the specified content height.
469     *
470     * @param contentHeight  the content height.
471     *
472     * @return The adjusted height.
473     */
474    protected double calculateTotalHeight(double contentHeight) {
475        double result = contentHeight;
476        result = this.padding.extendHeight(result);
477        result = this.frame.getInsets().extendHeight(result);
478        result = this.margin.extendHeight(result);
479        return result;
480    }
481
482    /**
483     * Reduces the specified area by the amount of space consumed
484     * by the margin.
485     *
486     * @param area  the area ({@code null} not permitted).
487     *
488     * @return The trimmed area.
489     */
490    protected Rectangle2D trimMargin(Rectangle2D area) {
491        // defer argument checking...
492        this.margin.trim(area);
493        return area;
494    }
495
496    /**
497     * Reduces the specified area by the amount of space consumed
498     * by the border.
499     *
500     * @param area  the area ({@code null} not permitted).
501     *
502     * @return The trimmed area.
503     */
504    protected Rectangle2D trimBorder(Rectangle2D area) {
505        // defer argument checking...
506        this.frame.getInsets().trim(area);
507        return area;
508    }
509
510    /**
511     * Reduces the specified area by the amount of space consumed
512     * by the padding.
513     *
514     * @param area  the area ({@code null} not permitted).
515     *
516     * @return The trimmed area.
517     */
518    protected Rectangle2D trimPadding(Rectangle2D area) {
519        // defer argument checking...
520        this.padding.trim(area);
521        return area;
522    }
523
524    /**
525     * Draws the border around the perimeter of the specified area.
526     *
527     * @param g2  the graphics device.
528     * @param area  the area.
529     */
530    protected void drawBorder(Graphics2D g2, Rectangle2D area) {
531        this.frame.draw(g2, area);
532    }
533
534    /**
535     * Tests this block for equality with an arbitrary object.
536     *
537     * @param obj  the object ({@code null} permitted).
538     *
539     * @return A boolean.
540     */
541    @Override
542    public boolean equals(Object obj) {
543        if (obj == this) {
544            return true;
545        }
546        if (!(obj instanceof AbstractBlock)) {
547            return false;
548        }
549        AbstractBlock that = (AbstractBlock) obj;
550        if (!Objects.equals(this.id, that.id)) {
551            return false;
552        }
553        if (!Objects.equals(this.frame, that.frame)) {
554            return false;
555        }
556        if (!Objects.equals(this.bounds, that.bounds)) {
557            return false;
558        }
559        if (!Objects.equals(this.margin, that.margin)) {
560            return false;
561        }
562        if (!Objects.equals(this.padding, that.padding)) {
563            return false;
564        }
565        if (Double.doubleToLongBits(this.height) !=
566            Double.doubleToLongBits(that.height)) {
567            return false;
568        }
569        if (Double.doubleToLongBits(this.width) !=
570            Double.doubleToLongBits(that.width)) {
571            return false;
572        }
573        
574        // fix the "equals not symmetric" problem
575        if (!that.canEqual(this)) {
576            return false;
577        }
578        return true;
579    }
580
581    /**
582     * Ensures symmetry between super/subclass implementations of equals. For
583     * more detail, see http://jqno.nl/equalsverifier/manual/inheritance.
584     *
585     * @param other Object
586     * 
587     * @return true ONLY if the parameter is THIS class type
588     */
589    public boolean canEqual(Object other) {
590        // fix the "equals not symmetric" problem
591        return (other instanceof AbstractBlock);
592    }
593
594    @Override
595    public int hashCode() {
596        int hash = 3;
597        hash = 89 * hash + Objects.hashCode(this.id);
598        hash = 89 * hash + Objects.hashCode(this.margin);
599        hash = 89 * hash + Objects.hashCode(this.frame);
600        hash = 89 * hash + Objects.hashCode(this.padding);
601        hash = 89 * hash + Objects.hashCode(this.bounds);
602        hash = 89 * hash +
603               (int) (Double.doubleToLongBits(this.width) ^
604                     (Double.doubleToLongBits(this.width) >>> 32));
605        hash = 89 * hash +
606               (int) (Double.doubleToLongBits(this.height) ^
607                     (Double.doubleToLongBits(this.height) >>> 32));
608        return hash;
609    }
610
611    /**
612     * Returns a clone of this block.
613     *
614     * @return A clone.
615     *
616     * @throws CloneNotSupportedException if there is a problem creating the
617     *         clone.
618     */
619    @Override
620    public Object clone() throws CloneNotSupportedException {
621        AbstractBlock clone = (AbstractBlock) super.clone();
622        clone.bounds = (Rectangle2D) ShapeUtils.clone(this.bounds);
623        if (this.frame instanceof PublicCloneable) {
624            PublicCloneable pc = (PublicCloneable) this.frame;
625            clone.frame = (BlockFrame) pc.clone();
626        }
627        return clone;
628    }
629
630    /**
631     * Provides serialization support.
632     *
633     * @param stream  the output stream.
634     *
635     * @throws IOException if there is an I/O error.
636     */
637    private void writeObject(ObjectOutputStream stream) throws IOException {
638        stream.defaultWriteObject();
639        SerialUtils.writeShape(this.bounds, stream);
640    }
641
642    /**
643     * Provides serialization support.
644     *
645     * @param stream  the input stream.
646     *
647     * @throws IOException  if there is an I/O error.
648     * @throws ClassNotFoundException  if there is a classpath problem.
649     */
650    private void readObject(ObjectInputStream stream)
651        throws IOException, ClassNotFoundException {
652        stream.defaultReadObject();
653        this.bounds = (Rectangle2D) SerialUtils.readShape(stream);
654    }
655
656}