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