001/*
002 * SVG Salamander
003 * Copyright (c) 2004, Mark McKay
004 * All rights reserved.
005 *
006 * Redistribution and use in source and binary forms, with or 
007 * without modification, are permitted provided that the following
008 * conditions are met:
009 *
010 *   - Redistributions of source code must retain the above 
011 *     copyright notice, this list of conditions and the following
012 *     disclaimer.
013 *   - Redistributions in binary form must reproduce the above
014 *     copyright notice, this list of conditions and the following
015 *     disclaimer in the documentation and/or other materials 
016 *     provided with the distribution.
017 *
018 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
019 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
020 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
021 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
022 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
023 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
025 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
026 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
027 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
028 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
029 * OF THE POSSIBILITY OF SUCH DAMAGE. 
030 * 
031 * Mark McKay can be contacted at mark@kitfox.com.  Salamander and other
032 * projects can be found at http://www.kitfox.com
033 *
034 * Created on January 26, 2004, 1:56 AM
035 */
036package com.kitfox.svg;
037
038import com.kitfox.svg.xml.StyleAttribute;
039import java.awt.Graphics2D;
040import java.awt.Shape;
041import java.awt.geom.AffineTransform;
042import java.awt.geom.Area;
043import java.awt.geom.NoninvertibleTransformException;
044import java.awt.geom.Point2D;
045import java.awt.geom.Rectangle2D;
046import java.util.Iterator;
047import java.util.List;
048
049/**
050 * @author Mark McKay
051 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a>
052 */
053public class Group extends ShapeElement
054{
055    public static final String TAG_NAME = "group";
056    
057    //Cache bounding box for faster clip testing
058    Rectangle2D boundingBox;
059    Shape cachedShape;
060
061    /**
062     * Creates a new instance of Stop
063     */
064    public Group()
065    {
066    }
067
068    @Override
069    public String getTagName()
070    {
071        return TAG_NAME;
072    }
073
074    /**
075     * Called after the start element but before the end element to indicate
076     * each child tag that has been processed
077     */
078    @Override
079    public void loaderAddChild(SVGLoaderHelper helper, SVGElement child) throws SVGElementException
080    {
081        super.loaderAddChild(helper, child);
082    }
083
084    protected boolean outsideClip(Graphics2D g) throws SVGException
085    {
086        Shape clip = g.getClip();
087        if (clip == null)
088        {
089            return false;
090        }
091        //g.getClipBounds(clipBounds);
092        Rectangle2D rect = getBoundingBox();
093
094        if (clip.intersects(rect))
095        {
096            return false;
097        }
098
099        return true;
100    }
101
102    @Override
103    void pick(Point2D point, boolean boundingBox, List<List<SVGElement>> retVec) throws SVGException
104    {
105        Point2D xPoint = new Point2D.Double(point.getX(), point.getY());
106        if (xform != null)
107        {
108            try
109            {
110                xform.inverseTransform(point, xPoint);
111            } catch (NoninvertibleTransformException ex)
112            {
113                throw new SVGException(ex);
114            }
115        }
116
117
118        for (SVGElement ele : children) {
119            if (ele instanceof RenderableElement)
120            {
121                RenderableElement rendEle = (RenderableElement) ele;
122
123                rendEle.pick(xPoint, boundingBox, retVec);
124            }
125        }
126    }
127
128    @Override
129    void pick(Rectangle2D pickArea, AffineTransform ltw, boolean boundingBox, List<List<SVGElement>> retVec) throws SVGException
130    {
131        if (xform != null)
132        {
133            ltw = new AffineTransform(ltw);
134            ltw.concatenate(xform);
135        }
136
137
138        for (SVGElement ele : children) {
139            if (ele instanceof RenderableElement)
140            {
141                RenderableElement rendEle = (RenderableElement) ele;
142
143                rendEle.pick(pickArea, ltw, boundingBox, retVec);
144            }
145        }
146    }
147
148    @Override
149    public void render(Graphics2D g) throws SVGException
150    {
151        //Don't process if not visible
152        StyleAttribute styleAttrib = new StyleAttribute();
153        //Visibility can be overridden by children
154
155        if (getStyle(styleAttrib.setName("display")))
156        {
157            if (styleAttrib.getStringValue().equals("none"))
158            {
159                return;
160            }
161        }
162        
163        //Do not process offscreen groups
164        boolean ignoreClip = diagram.ignoringClipHeuristic();
165//        if (!ignoreClip && outsideClip(g))
166//        {
167//            return;
168//        }
169
170        beginLayer(g);
171
172        Iterator<SVGElement> it = children.iterator();
173
174//        try
175//        {
176//            g.getClipBounds(clipBounds);
177//        }
178//        catch (Exception e)
179//        {
180//            //For some reason, getClipBounds can throw a null pointer exception for
181//            // some types of Graphics2D
182//            ignoreClip = true;
183//        }
184
185        Shape clip = g.getClip();
186        while (it.hasNext())
187        {
188            SVGElement ele = it.next();
189            if (ele instanceof RenderableElement)
190            {
191                RenderableElement rendEle = (RenderableElement) ele;
192
193//                if (shapeEle == null) continue;
194
195                if (!(ele instanceof Group))
196                {
197                    //Skip if clipping area is outside our bounds
198                    if (!ignoreClip && clip != null
199                        && !clip.intersects(rendEle.getBoundingBox()))
200                    {
201                        continue;
202                    }
203                }
204
205                rendEle.render(g);
206            }
207        }
208
209        finishLayer(g);
210    }
211
212    /**
213     * Retrieves the cached bounding box of this group
214     */
215    @Override
216    public Shape getShape()
217    {
218        if (cachedShape == null)
219        {
220            calcShape();
221        }
222        return cachedShape;
223    }
224
225    public void calcShape()
226    {
227        Area retShape = new Area();
228
229        for (SVGElement ele : children) {
230            if (ele instanceof ShapeElement)
231            {
232                ShapeElement shpEle = (ShapeElement) ele;
233                Shape shape = shpEle.getShape();
234                if (shape != null)
235                {
236                    retShape.add(new Area(shape));
237                }
238            }
239        }
240
241        cachedShape = shapeToParent(retShape);
242    }
243
244    /**
245     * Retrieves the cached bounding box of this group
246     */
247    @Override
248    public Rectangle2D getBoundingBox() throws SVGException
249    {
250        if (boundingBox == null)
251        {
252            calcBoundingBox();
253        }
254//        calcBoundingBox();
255        return boundingBox;
256    }
257
258    /**
259     * Recalculates the bounding box by taking the union of the bounding boxes
260     * of all children. Caches the result.
261     * @throws com.kitfox.svg.SVGException
262     */
263    public void calcBoundingBox() throws SVGException
264    {
265        Rectangle2D retRect = null;
266
267        for (SVGElement ele : children) {
268            if (ele instanceof RenderableElement)
269            {
270                RenderableElement rendEle = (RenderableElement) ele;
271                Rectangle2D bounds = rendEle.getBoundingBox();
272                if (bounds != null && (bounds.getWidth() != 0 || bounds.getHeight() != 0))
273                {
274                    if (retRect == null)
275                    {
276                        retRect = bounds;
277                    }
278                    else
279                    {
280                        if (retRect.getWidth() != 0 || retRect.getHeight() != 0)
281                        {
282                            retRect = retRect.createUnion(bounds);
283                        }
284                    }
285                }
286            }
287        }
288
289//        if (xform != null)
290//        {
291//            retRect = xform.createTransformedShape(retRect).getBounds2D();
292//        }
293
294        //If no contents, use degenerate rectangle
295        if (retRect == null)
296        {
297            retRect = new Rectangle2D.Float();
298        }
299
300        boundingBox = boundsToParent(retRect);
301    }
302
303    @Override
304    public boolean updateTime(double curTime) throws SVGException
305    {
306        boolean changeState = super.updateTime(curTime);
307        Iterator<SVGElement> it = children.iterator();
308
309        //Distribute message to all members of this group
310        while (it.hasNext())
311        {
312            SVGElement ele = it.next();
313            boolean updateVal = ele.updateTime(curTime);
314
315            changeState = changeState || updateVal;
316
317            //Update our shape if shape aware children change
318            if (ele instanceof ShapeElement)
319            {
320                cachedShape = null;
321            }
322            if (ele instanceof RenderableElement)
323            {
324                boundingBox = null;
325            }
326        }
327
328        return changeState;
329    }
330}