001    /**
002     * Copyright (c) 2009, Piet Blok
003     * All rights reserved.
004     *
005     * Redistribution and use in source and binary forms, with or without
006     * modification, are permitted provided that the following conditions
007     * are met:
008     *
009     *   * Redistributions of source code must retain the above copyright
010     *     notice, this list of conditions and the following disclaimer.
011     *   * Redistributions in binary form must reproduce the above
012     *     copyright notice, this list of conditions and the following
013     *     disclaimer in the documentation and/or other materials provided
014     *     with the distribution.
015     *   * Neither the name of the copyright holder nor the names of the
016     *     contributors may be used to endorse or promote products derived
017     *     from this software without specific prior written permission.
018     *
019     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
020     * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
021     * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
022     * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
023     * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
024     * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
025     * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
026     * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
027     * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
028     * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
029     * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
030     */
031    
032    package org.pbjar.jxlayer.plaf.ext.transform;
033    
034    import java.awt.Dimension;
035    import java.awt.geom.AffineTransform;
036    import java.awt.geom.Area;
037    import java.awt.geom.Rectangle2D;
038    import java.util.Arrays;
039    import java.util.Map;
040    import java.util.WeakHashMap;
041    
042    import javax.swing.JComponent;
043    import javax.swing.event.ChangeEvent;
044    import javax.swing.event.ChangeListener;
045    
046    import org.jdesktop.jxlayer.JXLayer;
047    
048    /**
049     * This is an implementation of {@link TransformModel} with methods to
050     * explicitly set transformation values.
051     * 
052     * @author Piet Blok
053     */
054    public class DefaultTransformModel implements TransformModel {
055    
056        /**
057         * Enum for internal convenience.
058         * 
059         * Describes the values that on change trigger recalculation of the
060         * transform. All have a default value, used for initializing arrays.
061         * 
062         * These enums are used for two purposes:
063         * 
064         * 1: To easily detect a change that requires renewed calculation of the
065         * transform (both program values and user options).
066         * 
067         * 2: To generalize setters (both program values and user options) and
068         * getters (only userOptions) for the various values.
069         * 
070         * There are two groups:
071         * 
072         * 1: Program values that reflect the current size etc. of affected
073         * components
074         * 
075         * 2: User options
076         */
077        private enum Type {
078            /*
079             * Program values
080             */
081            LayerWidth(0),
082    
083            LayerHeight(0),
084    
085            ViewWidth(0),
086    
087            ViewHeight(0),
088    
089            /*
090             * User options
091             */
092            PreferredScale(1.0),
093    
094            Rotation(0.0),
095    
096            ShearX(0.0),
097    
098            ShearY(0.0),
099    
100            QuadrantRotation(0),
101    
102            PreserveAspectRatio(Boolean.TRUE),
103    
104            ScaleToPreferredSize(Boolean.FALSE),
105    
106            Mirror(Boolean.FALSE),
107    
108            ;
109    
110            public static Object[] createArray() {
111                Object[] array = new Object[values().length];
112                for (Type type : values()) {
113                    array[type.ordinal()] = type.defaultValue;
114                }
115                return array;
116            }
117    
118            private Object defaultValue;
119    
120            private Type(Object defaultValue) {
121                this.defaultValue = defaultValue;
122            }
123        }
124    
125        private final Map<ChangeListener, Object> listeners = new WeakHashMap<ChangeListener, Object>();
126    
127        /**
128         * The transform object that will be recalculated upon any change.
129         */
130        private final AffineTransform transform = new AffineTransform();
131    
132        /**
133         * Is populated with the current values.
134         */
135        private final Object[] values = Type.createArray();
136    
137        /**
138         * Is populated with the previous values.
139         */
140        private final Object[] prevValues = Type.createArray();
141    
142        @Override
143        public void addChangeListener(ChangeListener listener) {
144            listeners.put(listener, null);
145        }
146    
147        /**
148         * Get the scale.
149         * 
150         * @return the scale
151         * @see #setScale(double)
152         */
153        public double getScale() {
154            return getValue(Type.PreferredScale);
155        }
156    
157        @Override
158        public AffineTransform getPreferredTransform(Dimension size,
159                JXLayer<?> layer) {
160            double centerX = size == null ? 0 : size.getWidth() / 2.0;
161            double centerY = size == null ? 0 : size.getHeight() / 2.0;
162            AffineTransform transform = transformNoScale(centerX, centerY);
163            double scaleX = getValue(Type.PreferredScale);
164            double scaleY = scaleX;
165            transform.translate(centerX, centerY);
166            transform.scale(getValue(Type.Mirror) ? -scaleX : scaleX, scaleY);
167            transform.translate(-centerX, -centerY);
168            return transform;
169        }
170    
171        /**
172         * Get the quadrant rotation value. The default value is {@code 0}.
173         * 
174         * @return the quadrant rotation value
175         * @see #setQuadrantRotation(int)
176         */
177        public int getQuadrantRotation() {
178            return getValue(Type.QuadrantRotation);
179        }
180    
181        /**
182         * Get the rotation value in radians as set by {@link #setRotation(double)}.
183         * The default value is {@code 0}.
184         * 
185         * @return the rotation value.
186         * @see #setRotation(double)
187         */
188        public double getRotation() {
189            return getValue(Type.Rotation);
190        }
191    
192        /**
193         * Get the shearX value as set by {@link #setShearX(double)}; The default
194         * value is {@code 0}.
195         * 
196         * @return the shear x value
197         * @see #setShearX(double)
198         */
199        public double getShearX() {
200            return getValue(Type.ShearX);
201        }
202    
203        /**
204         * Get the shearY value as set by {@link #setShearY(double)}; The default
205         * value is {@code 0}.
206         * 
207         * @return the shear y value
208         * @see #setShearY(double)
209         */
210        public double getShearY() {
211            return getValue(Type.ShearY);
212        }
213    
214        /**
215         * Return the currently active {@link AffineTransform}. Recalculate if
216         * needed.
217         * 
218         * @return the currently active {@link AffineTransform}
219         */
220        @Override
221        public AffineTransform getTransform(JXLayer<?> layer) {
222            JComponent view = layer.getView();
223            /*
224             * Set the current actual program values in addition to the user
225             * options.
226             */
227            setValue(Type.LayerWidth, layer == null ? 0 : layer.getWidth());
228            setValue(Type.LayerHeight, layer == null ? 0 : layer.getHeight());
229            setValue(Type.ViewWidth, view == null ? 0 : view.getWidth());
230            setValue(Type.ViewHeight, view == null ? 0 : view.getHeight());
231            /*
232             * If any change to previous values, recompute the transform.
233             */
234            if (!Arrays.equals(prevValues, values)) {
235                System.arraycopy(values, 0, prevValues, 0, values.length);
236                transform.setToIdentity();
237                if (view != null) {
238                    double centerX = layer == null ? 0 : layer.getWidth() / 2.0;
239                    double centerY = layer == null ? 0 : layer.getHeight() / 2.0;
240    
241                    AffineTransform nonScaledTransform = transformNoScale(centerX,
242                            centerY);
243    
244                    double scaleX, scaleY;
245                    if (getValue(Type.ScaleToPreferredSize)) {
246                        scaleX = getValue(Type.PreferredScale);
247                        scaleY = scaleX;
248                    } else {
249                        Area area = new Area(new Rectangle2D.Double(0, 0, view
250                                .getWidth(), view.getHeight()));
251                        area.transform(nonScaledTransform);
252                        Rectangle2D bounds = area.getBounds2D();
253                        scaleX = layer == null ? 0 : layer.getWidth()
254                                / bounds.getWidth();
255                        scaleY = layer == null ? 0 : layer.getHeight()
256                                / bounds.getHeight();
257    
258                        if (getValue(Type.PreserveAspectRatio)) {
259                            scaleX = Math.min(scaleX, scaleY);
260                            scaleY = scaleX;
261                        }
262                    }
263    
264                    transform.translate(centerX, centerY);
265                    transform.scale(getValue(Type.Mirror) ? -scaleX : scaleX,
266                            scaleY);
267                    transform.translate(-centerX, -centerY);
268    
269                    transform.concatenate(nonScaledTransform);
270                }
271            }
272            return transform;
273        }
274    
275        /**
276         * Get the mirror property.
277         * <p>
278         * The default value is {@code false}.
279         * </p>
280         * 
281         * @return {@code true} if the transformation will mirror the view.
282         * @see #setMirror(boolean)
283         */
284        public boolean isMirror() {
285            return getValue(Type.Mirror);
286        }
287    
288        /**
289         * Get the preserve aspect ratio value.
290         * <p>
291         * The default value is {@code true}.
292         * </p>
293         * 
294         * @return {@code true} if preserving aspect ratio, {@code false} otherwise
295         * @see #setPreserveAspectRatio(boolean)
296         */
297        public boolean isPreserveAspectRatio() {
298            return getValue(Type.PreserveAspectRatio);
299        }
300    
301        /**
302         * Get the scale to preferred size value.
303         * <p>
304         * The default value is {@code false}.
305         * </p>
306         * <p>
307         * When {@code true}, the view is scaled according to the preferred scale,
308         * regardless of the size of the {@link JXLayer}.
309         * </p>
310         * <p>
311         * When {@code false}, the view is scaled to occupy as much as possible of
312         * the size of the {@link JXLayer}.
313         * </p>
314         * 
315         * @return {@code true} if scale to preferred size, {@code false} otherwise
316         * @see #setScaleToPreferredSize(boolean)
317         */
318        public boolean isScaleToPreferredSize() {
319            return getValue(Type.ScaleToPreferredSize);
320        }
321    
322        @Override
323        public void removeChangeListener(ChangeListener listener) {
324            listeners.remove(listener);
325        }
326    
327        /**
328         * Set the mirror property.
329         * <p>
330         * The default value is {@code false}
331         * </p>
332         * 
333         * @param newValue
334         *            the new value
335         * @see #isMirror()
336         */
337        public void setMirror(boolean newValue) {
338            setValue(Type.Mirror, newValue);
339        }
340    
341        /**
342         * Set a scale.
343         * <p>
344         * The scale is primarily used to calculate a preferred size. Unless {@code
345         * ScaleToPreferredSize} is set to {@code true} (see
346         * {@link #setScaleToPreferredSize(boolean)} and
347         * {@link #isScaleToPreferredSize()}), actual scaling itself is calculated
348         * such that the view occupies as much space as possible on the
349         * {@link JXLayer}.
350         * </p>
351         * <p>
352         * The default value is 1.
353         * </p>
354         * 
355         * @param newValue
356         *            the preferred scale
357         * @throws IllegalArgumentException
358         *             when the argument value is 0
359         * @see #getScale()
360         */
361        public void setScale(double newValue) throws IllegalArgumentException {
362            if (newValue == 0.0) {
363                throw new IllegalArgumentException(
364                        "Preferred scale can not be set to 0");
365            }
366            setValue(Type.PreferredScale, newValue);
367        }
368    
369        /**
370         * Set preserve aspect ratio.
371         * <p>
372         * The default value is {@code true}.
373         * </p>
374         * 
375         * @param newValue
376         *            the new value
377         * @see #isPreserveAspectRatio()
378         */
379        public void setPreserveAspectRatio(boolean newValue) {
380            setValue(Type.PreserveAspectRatio, newValue);
381        }
382    
383        /**
384         * Set the rotation in quadrants. The default value is {@code 0}.
385         * 
386         * @param newValue
387         *            the number of quadrants
388         * @see #getQuadrantRotation()
389         */
390        public void setQuadrantRotation(int newValue) {
391            setValue(Type.QuadrantRotation, newValue);
392        }
393    
394        /**
395         * Set the rotation in radians. The default value is {@code 0}.
396         * 
397         * @param newValue
398         *            the rotation in radians
399         * @see #getRotation()
400         */
401        public void setRotation(double newValue) {
402            setValue(Type.Rotation, newValue);
403        }
404    
405        /**
406         * Set scaleToPreferredSize.
407         * <p>
408         * The default value is {@code false}.
409         * </p>
410         * <p>
411         * When {@code true}, the view is scaled according to the preferred scale,
412         * regardless of the size of the {@link JXLayer}.
413         * </p>
414         * <p>
415         * When {@code false}, the view is scaled to occupy as much as possible of
416         * the size of the {@link JXLayer}.
417         * </p>
418         * 
419         * @param newValue
420         *            the new value
421         * @see #isScaleToPreferredSize()
422         */
423        public void setScaleToPreferredSize(boolean newValue) {
424            setValue(Type.ScaleToPreferredSize, newValue);
425        }
426    
427        /**
428         * Set the shearX value. The default value is {@code 0}.
429         * 
430         * @param newValue
431         *            the shear x
432         * @see #getShearX()
433         */
434        public void setShearX(double newValue) {
435            setValue(Type.ShearX, newValue);
436        }
437    
438        /**
439         * Set the shearY value. The default value is {@code 0}.
440         * 
441         * @param newValue
442         *            the shear y
443         * @see #getShearY()
444         */
445        public void setShearY(double newValue) {
446            setValue(Type.ShearY, newValue);
447        }
448    
449        /**
450         * If {!oldValue.equals(newValue)}, a {@link ChangeEvent} will be fired.
451         * 
452         * @param oldValue
453         *            an old value
454         * @param newValue
455         *            a new value
456         */
457        protected void fireChangeEvent(Object oldValue, Object newValue) {
458            if (!oldValue.equals(newValue)) {
459                ChangeEvent event = new ChangeEvent(this);
460                for (ChangeListener listener : listeners.keySet()) {
461                    listener.stateChanged(event);
462                }
463            }
464        }
465    
466        @SuppressWarnings("unchecked")
467        private <T> T getValue(Type type) {
468            return (T) values[type.ordinal()];
469        }
470    
471        /**
472         * Set a value and fire a PropertyChange.
473         * 
474         * @param type
475         *            the value type
476         * @param newValue
477         *            the new value
478         */
479        private void setValue(Type type, Object newValue) {
480            Object oldValue = values[type.ordinal()];
481            values[type.ordinal()] = newValue;
482            fireChangeEvent(oldValue, newValue);
483        }
484    
485        /**
486         * Apply the prescribed transformations, excluding the scale.
487         * 
488         * @param centerX
489         *            a center X
490         * @param centerY
491         *            a center Y
492         * @return a new {@link AffineTransform}
493         */
494        private AffineTransform transformNoScale(double centerX, double centerY) {
495            AffineTransform at = new AffineTransform();
496            at.translate(centerX, centerY);
497            at.rotate(getRotation());
498            at.quadrantRotate(getQuadrantRotation());
499            at.shear(getShearX(), getShearY());
500            at.translate(-centerX, -centerY);
501            return at;
502        }
503    
504    }