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.Graphics2D;
043import java.awt.Rectangle;
044import java.awt.Shape;
045import java.awt.geom.AffineTransform;
046import java.awt.geom.NoninvertibleTransformException;
047import java.awt.geom.Point2D;
048import java.awt.geom.Rectangle2D;
049import java.util.List;
050
051/**
052 * The root element of an SVG tree.
053 *
054 * @author Mark McKay
055 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a>
056 */
057public class SVGRoot extends Group
058{
059    public static final String TAG_NAME = "svg";
060
061    NumberWithUnits x;
062    NumberWithUnits y;
063    NumberWithUnits width;
064    NumberWithUnits height;
065
066    Rectangle2D.Float viewBox = null;
067
068    public static final int PA_X_NONE = 0;
069    public static final int PA_X_MIN = 1;
070    public static final int PA_X_MID = 2;
071    public static final int PA_X_MAX = 3;
072
073    public static final int PA_Y_NONE = 0;
074    public static final int PA_Y_MIN = 1;
075    public static final int PA_Y_MID = 2;
076    public static final int PA_Y_MAX = 3;
077
078    public static final int PS_MEET = 0;
079    public static final int PS_SLICE = 1;
080
081    int parSpecifier = PS_MEET;
082    int parAlignX = PA_X_MID;
083    int parAlignY = PA_Y_MID;
084
085    final AffineTransform viewXform = new AffineTransform();
086    final Rectangle2D.Float clipRect = new Rectangle2D.Float();
087
088    private StyleSheet styleSheet;
089    
090    /** Creates a new instance of SVGRoot */
091    public SVGRoot()
092    {
093    }
094
095    @Override
096    public String getTagName()
097    {
098        return TAG_NAME;
099    }
100    
101    @Override
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    @Override
168    public SVGRoot getRoot()
169    {
170        return this;
171    }
172    
173    protected void prepareViewport()
174    {
175        Rectangle deviceViewport = diagram.getDeviceViewport();
176        
177        Rectangle2D defaultBounds;
178        try
179        {
180            defaultBounds = getBoundingBox();
181        }
182        catch (SVGException ex)
183        {
184            defaultBounds= new Rectangle2D.Float();
185        }
186        
187        //Determine destination rectangle
188        float xx, yy, ww, hh;
189        if (width != null)
190        {
191            xx = (x == null) ? 0 : StyleAttribute.convertUnitsToPixels(x.getUnits(), x.getValue());
192            if (width.getUnits() == NumberWithUnits.UT_PERCENT)
193            {
194                ww = width.getValue() * deviceViewport.width;
195            }
196            else
197            {
198                ww = StyleAttribute.convertUnitsToPixels(width.getUnits(), width.getValue());
199            }
200        }
201        else if (viewBox != null)
202        {
203            xx = (float)viewBox.x;
204            ww = (float)viewBox.width;
205            width = new NumberWithUnits(ww, NumberWithUnits.UT_PX);
206            x = new NumberWithUnits(xx, NumberWithUnits.UT_PX);
207        }
208        else
209        {
210            //Estimate size from scene bounding box
211            xx = (float)defaultBounds.getX();
212            ww = (float)defaultBounds.getWidth();
213            width = new NumberWithUnits(ww, NumberWithUnits.UT_PX);
214            x = new NumberWithUnits(xx, NumberWithUnits.UT_PX);
215        }
216        
217        if (height != null)
218        {
219            yy = (y == null) ? 0 : StyleAttribute.convertUnitsToPixels(y.getUnits(), y.getValue());
220            if (height.getUnits() == NumberWithUnits.UT_PERCENT)
221            {
222                hh = height.getValue() * deviceViewport.height;
223            }
224            else
225            {
226                hh = StyleAttribute.convertUnitsToPixels(height.getUnits(), height.getValue());
227            }
228        }
229        else if (viewBox != null)
230        {
231            yy = (float)viewBox.y;
232            hh = (float)viewBox.height;
233            height = new NumberWithUnits(hh, NumberWithUnits.UT_PX);
234            y = new NumberWithUnits(yy, NumberWithUnits.UT_PX);
235        }
236        else
237        {
238            //Estimate size from scene bounding box
239            yy = (float)defaultBounds.getY();
240            hh = (float)defaultBounds.getHeight();
241            height = new NumberWithUnits(hh, NumberWithUnits.UT_PX);
242            y = new NumberWithUnits(yy, NumberWithUnits.UT_PX);
243        }
244
245        clipRect.setRect(xx, yy, ww, hh);
246
247//        if (viewBox == null)
248//        {
249//            viewXform.setToIdentity();
250//        }
251//        else
252//        {
253//            //If viewport window is set, we are drawing to entire viewport
254//            clipRect.setRect(deviceViewport);
255//            
256//            viewXform.setToIdentity();
257//            viewXform.setToTranslation(deviceViewport.x, deviceViewport.y);
258//            viewXform.scale(deviceViewport.width, deviceViewport.height);
259//            viewXform.scale(1 / viewBox.width, 1 / viewBox.height);
260//            viewXform.translate(-viewBox.x, -viewBox.y);
261//        }
262    }
263
264    public void renderToViewport(Graphics2D g) throws SVGException
265    {
266        prepareViewport();
267        
268        Rectangle targetViewport = g.getClipBounds();
269//
270//        if (targetViewport == null)
271//        {
272//            Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
273//            targetViewport = new Rectangle(0, 0, size.width, size.height);
274//        }
275//        clipRect.setRect(targetViewport);
276
277        
278        Rectangle deviceViewport = diagram.getDeviceViewport();
279        if (width != null && height != null)
280        {
281            float xx, yy, ww, hh;
282            
283            xx = (x == null) ? 0 : StyleAttribute.convertUnitsToPixels(x.getUnits(), x.getValue());
284            if (width.getUnits() == NumberWithUnits.UT_PERCENT)
285            {
286                ww = width.getValue() * deviceViewport.width;
287            }
288            else
289            {
290                ww = StyleAttribute.convertUnitsToPixels(width.getUnits(), width.getValue());
291            }
292            
293            yy = (y == null) ? 0 : StyleAttribute.convertUnitsToPixels(y.getUnits(), y.getValue());
294            if (height.getUnits() == NumberWithUnits.UT_PERCENT)
295            {
296                hh = height.getValue() * deviceViewport.height;
297            }
298            else
299            {
300                hh = StyleAttribute.convertUnitsToPixels(height.getUnits(), height.getValue());
301            }
302            
303            targetViewport = new Rectangle((int)xx, (int)yy, (int)ww, (int)hh);
304        }
305        else
306        {
307//            Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
308//            targetViewport = new Rectangle(0, 0, size.width, size.height);
309            targetViewport = new Rectangle(deviceViewport);
310        }
311        clipRect.setRect(targetViewport);
312
313        if (viewBox == null)
314        {
315            viewXform.setToIdentity();
316        }
317        else
318        {
319            viewXform.setToIdentity();
320            viewXform.setToTranslation(targetViewport.x, targetViewport.y);
321            viewXform.scale(targetViewport.width, targetViewport.height);
322            viewXform.scale(1 / viewBox.width, 1 / viewBox.height);
323            viewXform.translate(-viewBox.x, -viewBox.y);
324        }
325        
326        AffineTransform cachedXform = g.getTransform();
327        g.transform(viewXform);
328        
329        super.render(g);
330        
331        g.setTransform(cachedXform);
332    }
333
334    @Override
335    public void pick(Rectangle2D pickArea, AffineTransform ltw, boolean boundingBox, List<List<SVGElement>> retVec) throws SVGException
336    {
337        if (viewXform != null)
338        {
339            ltw = new AffineTransform(ltw);
340            ltw.concatenate(viewXform);
341        }
342        
343        super.pick(pickArea, ltw, boundingBox, retVec);
344    }
345    
346    @Override
347    public void pick(Point2D point, boolean boundingBox, List<List<SVGElement>> retVec) throws SVGException
348    {
349        Point2D xPoint = new Point2D.Double(point.getX(), point.getY());
350        if (viewXform != null)
351        {
352            try
353            {
354                viewXform.inverseTransform(point, xPoint);
355            } catch (NoninvertibleTransformException ex)
356            {
357                throw new SVGException(ex);
358            }
359        }
360        
361        super.pick(xPoint, boundingBox, retVec);
362    }
363
364    @Override
365    public Shape getShape()
366    {
367        Shape shape = super.getShape();
368        return viewXform.createTransformedShape(shape);
369    }
370
371    @Override
372    public Rectangle2D getBoundingBox() throws SVGException
373    {
374        Rectangle2D bbox = super.getBoundingBox();
375        return viewXform.createTransformedShape(bbox).getBounds2D();
376    }
377    
378    public float getDeviceWidth()
379    {
380        return clipRect.width;
381    }
382    
383    public float getDeviceHeight()
384    {
385        return clipRect.height;
386    }
387    
388    public Rectangle2D getDeviceRect(Rectangle2D rect)
389    {
390        rect.setRect(clipRect);
391        return rect;
392    }
393
394    /**
395     * Updates all attributes in this diagram associated with a time event.
396     * Ie, all attributes with track information.
397     * @return - true if this node has changed state as a result of the time
398     * update
399     */
400    @Override
401    public boolean updateTime(double curTime) throws SVGException
402    {
403        boolean changeState = super.updateTime(curTime);
404        
405        StyleAttribute sty = new StyleAttribute();
406        boolean shapeChange = false;
407        
408        if (getPres(sty.setName("x")))
409        {
410            NumberWithUnits newVal = sty.getNumberWithUnits();
411            if (!newVal.equals(x))
412            {
413                x = newVal;
414                shapeChange = true;
415            }
416        }
417
418        if (getPres(sty.setName("y")))
419        {
420            NumberWithUnits newVal = sty.getNumberWithUnits();
421            if (!newVal.equals(y))
422            {
423                y = newVal;
424                shapeChange = true;
425            }
426        }
427
428        if (getPres(sty.setName("width")))
429        {
430            NumberWithUnits newVal = sty.getNumberWithUnits();
431            if (!newVal.equals(width))
432            {
433                width = newVal;
434                shapeChange = true;
435            }
436        }
437
438        if (getPres(sty.setName("height")))
439        {
440            NumberWithUnits newVal = sty.getNumberWithUnits();
441            if (!newVal.equals(height))
442            {
443                height = newVal;
444                shapeChange = true;
445            }
446        }
447        
448        if (getPres(sty.setName("viewBox"))) 
449        {
450            float[] coords = sty.getFloatList();
451            Rectangle2D.Float newViewBox = new Rectangle2D.Float(coords[0], coords[1], coords[2], coords[3]);
452            if (!newViewBox.equals(viewBox))
453            {
454                viewBox = newViewBox;
455                shapeChange = true;
456            }
457        }
458
459        if (shapeChange)
460        {
461            build();
462        }
463
464        return changeState || shapeChange;
465    }
466
467    /**
468     * @return the styleSheet
469     */
470    public StyleSheet getStyleSheet()
471    {
472        if (styleSheet == null)
473        {
474            for (int i = 0; i < getNumChildren(); ++i)
475            {
476                SVGElement ele = getChild(i);
477                if (ele instanceof Style)
478                {
479                    return ((Style)ele).getStyleSheet();
480                }
481            }
482        }
483        
484        return styleSheet;
485    }
486
487    /**
488     * @param styleSheet the styleSheet to set
489     */
490    public void setStyleSheet(StyleSheet styleSheet)
491    {
492        this.styleSheet = styleSheet;
493    }
494
495}