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 February 18, 2004, 5:33 PM
035 */
036
037package com.kitfox.svg;
038
039import com.kitfox.svg.xml.NumberWithUnits;
040import com.kitfox.svg.xml.StyleAttribute;
041import com.kitfox.svg.xml.StyleSheet;
042import java.awt.Dimension;
043import java.awt.Graphics2D;
044import java.awt.Rectangle;
045import java.awt.Shape;
046import java.awt.Toolkit;
047import java.awt.geom.AffineTransform;
048import java.awt.geom.NoninvertibleTransformException;
049import java.awt.geom.Point2D;
050import java.awt.geom.Rectangle2D;
051import java.util.List;
052
053/**
054 * The root element of an SVG tree.
055 *
056 * @author Mark McKay
057 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a>
058 */
059public class SVGRoot extends Group
060{
061    public static final String TAG_NAME = "svg";
062
063    NumberWithUnits x;
064    NumberWithUnits y;
065    NumberWithUnits width;
066    NumberWithUnits height;
067
068    Rectangle2D.Float viewBox = null;
069
070    public static final int PA_X_NONE = 0;
071    public static final int PA_X_MIN = 1;
072    public static final int PA_X_MID = 2;
073    public static final int PA_X_MAX = 3;
074
075    public static final int PA_Y_NONE = 0;
076    public static final int PA_Y_MIN = 1;
077    public static final int PA_Y_MID = 2;
078    public static final int PA_Y_MAX = 3;
079
080    public static final int PS_MEET = 0;
081    public static final int PS_SLICE = 1;
082
083    int parSpecifier = PS_MEET;
084    int parAlignX = PA_X_MID;
085    int parAlignY = PA_Y_MID;
086
087    final AffineTransform viewXform = new AffineTransform();
088    final Rectangle2D.Float clipRect = new Rectangle2D.Float();
089
090    private StyleSheet styleSheet;
091    
092    /** Creates a new instance of SVGRoot */
093    public SVGRoot()
094    {
095    }
096
097    public String getTagName()
098    {
099        return TAG_NAME;
100    }
101    
102    public void build() throws SVGException
103    {
104        super.build();
105        
106        StyleAttribute sty = new StyleAttribute();
107        
108        if (getPres(sty.setName("x")))
109        {
110            x = sty.getNumberWithUnits();
111        }
112        
113        if (getPres(sty.setName("y")))
114        {
115            y = sty.getNumberWithUnits();
116        }
117        
118        if (getPres(sty.setName("width")))
119        {
120            width = sty.getNumberWithUnits();
121        }
122        
123        if (getPres(sty.setName("height")))
124        {
125            height = sty.getNumberWithUnits();
126        }
127        
128        if (getPres(sty.setName("viewBox"))) 
129        {
130            float[] coords = sty.getFloatList();
131            viewBox = new Rectangle2D.Float(coords[0], coords[1], coords[2], coords[3]);
132        }
133        
134        if (getPres(sty.setName("preserveAspectRatio")))
135        {
136            String preserve = sty.getStringValue();
137            
138            if (contains(preserve, "none")) { parAlignX = PA_X_NONE; parAlignY = PA_Y_NONE; }
139            else if (contains(preserve, "xMinYMin")) { parAlignX = PA_X_MIN; parAlignY = PA_Y_MIN; }
140            else if (contains(preserve, "xMidYMin")) { parAlignX = PA_X_MID; parAlignY = PA_Y_MIN; }
141            else if (contains(preserve, "xMaxYMin")) { parAlignX = PA_X_MAX; parAlignY = PA_Y_MIN; }
142            else if (contains(preserve, "xMinYMid")) { parAlignX = PA_X_MIN; parAlignY = PA_Y_MID; }
143            else if (contains(preserve, "xMidYMid")) { parAlignX = PA_X_MID; parAlignY = PA_Y_MID; }
144            else if (contains(preserve, "xMaxYMid")) { parAlignX = PA_X_MAX; parAlignY = PA_Y_MID; }
145            else if (contains(preserve, "xMinYMax")) { parAlignX = PA_X_MIN; parAlignY = PA_Y_MAX; }
146            else if (contains(preserve, "xMidYMax")) { parAlignX = PA_X_MID; parAlignY = PA_Y_MAX; }
147            else if (contains(preserve, "xMaxYMax")) { parAlignX = PA_X_MAX; parAlignY = PA_Y_MAX; }
148
149            if (contains(preserve, "meet"))
150            {
151                parSpecifier = PS_MEET;
152            }
153            else if (contains(preserve, "slice"))
154            {
155                parSpecifier = PS_SLICE;
156            }
157        }
158        
159        prepareViewport();
160    }
161    
162    private boolean contains(String text, String find) 
163    {
164        return (text.indexOf(find) != -1);
165    }
166
167    public SVGRoot getRoot()
168    {
169        return this;
170    }
171    
172    protected void prepareViewport()
173    {
174        Rectangle deviceViewport = diagram.getDeviceViewport();
175        
176        Rectangle2D defaultBounds;
177        try
178        {
179            defaultBounds = getBoundingBox();
180        }
181        catch (SVGException ex)
182        {
183            defaultBounds= new Rectangle2D.Float();
184        }
185        
186        //Determine destination rectangle
187        float xx, yy, ww, hh;
188        if (width != null)
189        {
190            xx = (x == null) ? 0 : StyleAttribute.convertUnitsToPixels(x.getUnits(), x.getValue());
191            if (width.getUnits() == NumberWithUnits.UT_PERCENT)
192            {
193                ww = width.getValue() * deviceViewport.width;
194            }
195            else
196            {
197                ww = StyleAttribute.convertUnitsToPixels(width.getUnits(), width.getValue());
198            }
199        }
200        else if (viewBox != null)
201        {
202            xx = (float)viewBox.x;
203            ww = (float)viewBox.width;
204            width = new NumberWithUnits(ww, NumberWithUnits.UT_PX);
205            x = new NumberWithUnits(xx, NumberWithUnits.UT_PX);
206        }
207        else
208        {
209            //Estimate size from scene bounding box
210            xx = (float)defaultBounds.getX();
211            ww = (float)defaultBounds.getWidth();
212            width = new NumberWithUnits(ww, NumberWithUnits.UT_PX);
213            x = new NumberWithUnits(xx, NumberWithUnits.UT_PX);
214        }
215        
216        if (height != null)
217        {
218            yy = (y == null) ? 0 : StyleAttribute.convertUnitsToPixels(y.getUnits(), y.getValue());
219            if (height.getUnits() == NumberWithUnits.UT_PERCENT)
220            {
221                hh = height.getValue() * deviceViewport.height;
222            }
223            else
224            {
225                hh = StyleAttribute.convertUnitsToPixels(height.getUnits(), height.getValue());
226            }
227        }
228        else if (viewBox != null)
229        {
230            yy = (float)viewBox.y;
231            hh = (float)viewBox.height;
232            height = new NumberWithUnits(hh, NumberWithUnits.UT_PX);
233            y = new NumberWithUnits(yy, NumberWithUnits.UT_PX);
234        }
235        else
236        {
237            //Estimate size from scene bounding box
238            yy = (float)defaultBounds.getY();
239            hh = (float)defaultBounds.getHeight();
240            height = new NumberWithUnits(hh, NumberWithUnits.UT_PX);
241            y = new NumberWithUnits(yy, NumberWithUnits.UT_PX);
242        }
243
244        clipRect.setRect(xx, yy, ww, hh);
245
246//        if (viewBox == null)
247//        {
248//            viewXform.setToIdentity();
249//        }
250//        else
251//        {
252//            //If viewport window is set, we are drawing to entire viewport
253//            clipRect.setRect(deviceViewport);
254//            
255//            viewXform.setToIdentity();
256//            viewXform.setToTranslation(deviceViewport.x, deviceViewport.y);
257//            viewXform.scale(deviceViewport.width, deviceViewport.height);
258//            viewXform.scale(1 / viewBox.width, 1 / viewBox.height);
259//            viewXform.translate(-viewBox.x, -viewBox.y);
260//        }
261    }
262
263    public void renderToViewport(Graphics2D g) throws SVGException
264    {
265        prepareViewport();
266
267        if (viewBox == null)
268        {
269            viewXform.setToIdentity();
270        }
271        else
272        {
273            Rectangle deviceViewport = g.getClipBounds();
274            if (deviceViewport == null)
275            {
276                Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
277                deviceViewport = new Rectangle(0, 0, size.width, size.height);
278            }
279            clipRect.setRect(deviceViewport);
280            
281            viewXform.setToIdentity();
282            viewXform.setToTranslation(deviceViewport.x, deviceViewport.y);
283            viewXform.scale(deviceViewport.width, deviceViewport.height);
284            viewXform.scale(1 / viewBox.width, 1 / viewBox.height);
285            viewXform.translate(-viewBox.x, -viewBox.y);
286        }
287        
288        AffineTransform cachedXform = g.getTransform();
289        g.transform(viewXform);
290        
291        super.render(g);
292        
293        g.setTransform(cachedXform);
294    }
295
296    public void pick(Rectangle2D pickArea, AffineTransform ltw, boolean boundingBox, List retVec) throws SVGException
297    {
298        if (viewXform != null)
299        {
300            ltw = new AffineTransform(ltw);
301            ltw.concatenate(viewXform);
302        }
303        
304        super.pick(pickArea, ltw, boundingBox, retVec);
305    }
306    
307    public void pick(Point2D point, boolean boundingBox, List retVec) throws SVGException
308    {
309        Point2D xPoint = new Point2D.Double(point.getX(), point.getY());
310        if (viewXform != null)
311        {
312            try
313            {
314                viewXform.inverseTransform(point, xPoint);
315            } catch (NoninvertibleTransformException ex)
316            {
317                throw new SVGException(ex);
318            }
319        }
320        
321        super.pick(xPoint, boundingBox, retVec);
322    }
323
324    public Shape getShape()
325    {
326        Shape shape = super.getShape();
327        return viewXform.createTransformedShape(shape);
328    }
329
330    public Rectangle2D getBoundingBox() throws SVGException
331    {
332        Rectangle2D bbox = super.getBoundingBox();
333        return viewXform.createTransformedShape(bbox).getBounds2D();
334    }
335    
336    public float getDeviceWidth()
337    {
338        return clipRect.width;
339    }
340    
341    public float getDeviceHeight()
342    {
343        return clipRect.height;
344    }
345    
346    public Rectangle2D getDeviceRect(Rectangle2D rect)
347    {
348        rect.setRect(clipRect);
349        return rect;
350    }
351
352    /**
353     * Updates all attributes in this diagram associated with a time event.
354     * Ie, all attributes with track information.
355     * @return - true if this node has changed state as a result of the time
356     * update
357     */
358    public boolean updateTime(double curTime) throws SVGException
359    {
360        boolean changeState = super.updateTime(curTime);
361        
362        StyleAttribute sty = new StyleAttribute();
363        boolean shapeChange = false;
364        
365        if (getPres(sty.setName("x")))
366        {
367            NumberWithUnits newVal = sty.getNumberWithUnits();
368            if (!newVal.equals(x))
369            {
370                x = newVal;
371                shapeChange = true;
372            }
373        }
374
375        if (getPres(sty.setName("y")))
376        {
377            NumberWithUnits newVal = sty.getNumberWithUnits();
378            if (!newVal.equals(y))
379            {
380                y = newVal;
381                shapeChange = true;
382            }
383        }
384
385        if (getPres(sty.setName("width")))
386        {
387            NumberWithUnits newVal = sty.getNumberWithUnits();
388            if (!newVal.equals(width))
389            {
390                width = newVal;
391                shapeChange = true;
392            }
393        }
394
395        if (getPres(sty.setName("height")))
396        {
397            NumberWithUnits newVal = sty.getNumberWithUnits();
398            if (!newVal.equals(height))
399            {
400                height = newVal;
401                shapeChange = true;
402            }
403        }
404        
405        if (getPres(sty.setName("viewBox"))) 
406        {
407            float[] coords = sty.getFloatList();
408            Rectangle2D.Float newViewBox = new Rectangle2D.Float(coords[0], coords[1], coords[2], coords[3]);
409            if (!newViewBox.equals(viewBox))
410            {
411                viewBox = newViewBox;
412                shapeChange = true;
413            }
414        }
415
416        if (shapeChange)
417        {
418            build();
419        }
420
421        return changeState || shapeChange;
422    }
423
424    /**
425     * @return the styleSheet
426     */
427    public StyleSheet getStyleSheet()
428    {
429        if (styleSheet == null)
430        {
431            for (int i = 0; i < getNumChildren(); ++i)
432            {
433                SVGElement ele = getChild(i);
434                if (ele instanceof Style)
435                {
436                    return ((Style)ele).getStyleSheet();
437                }
438            }
439        }
440        
441        return styleSheet;
442    }
443
444    /**
445     * @param styleSheet the styleSheet to set
446     */
447    public void setStyleSheet(StyleSheet styleSheet)
448    {
449        this.styleSheet = styleSheet;
450    }
451
452}