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        Rectangle targetViewport = g.getClipBounds();
268//
269//        if (targetViewport == null)
270//        {
271//            Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
272//            targetViewport = new Rectangle(0, 0, size.width, size.height);
273//        }
274//        clipRect.setRect(targetViewport);
275
276        
277        Rectangle deviceViewport = diagram.getDeviceViewport();
278        if (width != null && height != null)
279        {
280            float xx, yy, ww, hh;
281            
282            xx = (x == null) ? 0 : StyleAttribute.convertUnitsToPixels(x.getUnits(), x.getValue());
283            if (width.getUnits() == NumberWithUnits.UT_PERCENT)
284            {
285                ww = width.getValue() * deviceViewport.width;
286            }
287            else
288            {
289                ww = StyleAttribute.convertUnitsToPixels(width.getUnits(), width.getValue());
290            }
291            
292            yy = (y == null) ? 0 : StyleAttribute.convertUnitsToPixels(y.getUnits(), y.getValue());
293            if (height.getUnits() == NumberWithUnits.UT_PERCENT)
294            {
295                hh = height.getValue() * deviceViewport.height;
296            }
297            else
298            {
299                hh = StyleAttribute.convertUnitsToPixels(height.getUnits(), height.getValue());
300            }
301            
302            targetViewport = new Rectangle((int)xx, (int)yy, (int)ww, (int)hh);
303        }
304        else
305        {
306//            Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
307//            targetViewport = new Rectangle(0, 0, size.width, size.height);
308            targetViewport = new Rectangle(deviceViewport);
309        }
310        clipRect.setRect(targetViewport);
311
312        if (viewBox == null)
313        {
314            viewXform.setToIdentity();
315        }
316        else
317        {
318            viewXform.setToIdentity();
319            viewXform.setToTranslation(targetViewport.x, targetViewport.y);
320            viewXform.scale(targetViewport.width, targetViewport.height);
321            viewXform.scale(1 / viewBox.width, 1 / viewBox.height);
322            viewXform.translate(-viewBox.x, -viewBox.y);
323        }
324        
325        AffineTransform cachedXform = g.getTransform();
326        g.transform(viewXform);
327        
328        super.render(g);
329        
330        g.setTransform(cachedXform);
331    }
332
333    public void pick(Rectangle2D pickArea, AffineTransform ltw, boolean boundingBox, List retVec) throws SVGException
334    {
335        if (viewXform != null)
336        {
337            ltw = new AffineTransform(ltw);
338            ltw.concatenate(viewXform);
339        }
340        
341        super.pick(pickArea, ltw, boundingBox, retVec);
342    }
343    
344    public void pick(Point2D point, boolean boundingBox, List retVec) throws SVGException
345    {
346        Point2D xPoint = new Point2D.Double(point.getX(), point.getY());
347        if (viewXform != null)
348        {
349            try
350            {
351                viewXform.inverseTransform(point, xPoint);
352            } catch (NoninvertibleTransformException ex)
353            {
354                throw new SVGException(ex);
355            }
356        }
357        
358        super.pick(xPoint, boundingBox, retVec);
359    }
360
361    public Shape getShape()
362    {
363        Shape shape = super.getShape();
364        return viewXform.createTransformedShape(shape);
365    }
366
367    public Rectangle2D getBoundingBox() throws SVGException
368    {
369        Rectangle2D bbox = super.getBoundingBox();
370        return viewXform.createTransformedShape(bbox).getBounds2D();
371    }
372    
373    public float getDeviceWidth()
374    {
375        return clipRect.width;
376    }
377    
378    public float getDeviceHeight()
379    {
380        return clipRect.height;
381    }
382    
383    public Rectangle2D getDeviceRect(Rectangle2D rect)
384    {
385        rect.setRect(clipRect);
386        return rect;
387    }
388
389    /**
390     * Updates all attributes in this diagram associated with a time event.
391     * Ie, all attributes with track information.
392     * @return - true if this node has changed state as a result of the time
393     * update
394     */
395    public boolean updateTime(double curTime) throws SVGException
396    {
397        boolean changeState = super.updateTime(curTime);
398        
399        StyleAttribute sty = new StyleAttribute();
400        boolean shapeChange = false;
401        
402        if (getPres(sty.setName("x")))
403        {
404            NumberWithUnits newVal = sty.getNumberWithUnits();
405            if (!newVal.equals(x))
406            {
407                x = newVal;
408                shapeChange = true;
409            }
410        }
411
412        if (getPres(sty.setName("y")))
413        {
414            NumberWithUnits newVal = sty.getNumberWithUnits();
415            if (!newVal.equals(y))
416            {
417                y = newVal;
418                shapeChange = true;
419            }
420        }
421
422        if (getPres(sty.setName("width")))
423        {
424            NumberWithUnits newVal = sty.getNumberWithUnits();
425            if (!newVal.equals(width))
426            {
427                width = newVal;
428                shapeChange = true;
429            }
430        }
431
432        if (getPres(sty.setName("height")))
433        {
434            NumberWithUnits newVal = sty.getNumberWithUnits();
435            if (!newVal.equals(height))
436            {
437                height = newVal;
438                shapeChange = true;
439            }
440        }
441        
442        if (getPres(sty.setName("viewBox"))) 
443        {
444            float[] coords = sty.getFloatList();
445            Rectangle2D.Float newViewBox = new Rectangle2D.Float(coords[0], coords[1], coords[2], coords[3]);
446            if (!newViewBox.equals(viewBox))
447            {
448                viewBox = newViewBox;
449                shapeChange = true;
450            }
451        }
452
453        if (shapeChange)
454        {
455            build();
456        }
457
458        return changeState || shapeChange;
459    }
460
461    /**
462     * @return the styleSheet
463     */
464    public StyleSheet getStyleSheet()
465    {
466        if (styleSheet == null)
467        {
468            for (int i = 0; i < getNumChildren(); ++i)
469            {
470                SVGElement ele = getChild(i);
471                if (ele instanceof Style)
472                {
473                    return ((Style)ele).getStyleSheet();
474                }
475            }
476        }
477        
478        return styleSheet;
479    }
480
481    /**
482     * @param styleSheet the styleSheet to set
483     */
484    public void setStyleSheet(StyleSheet styleSheet)
485    {
486        this.styleSheet = styleSheet;
487    }
488
489}