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.font.FontRenderContext;
042import java.awt.geom.AffineTransform;
043import java.awt.geom.GeneralPath;
044import java.awt.geom.Rectangle2D;
045import java.util.Iterator;
046import java.util.LinkedList;
047import java.util.regex.Matcher;
048import java.util.regex.Pattern;
049
050//import org.apache.batik.ext.awt.geom.ExtendedGeneralPath;
051/**
052 * @author Mark McKay
053 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a>
054 */
055public class Text extends ShapeElement
056{
057    public static final String TAG_NAME = "text";
058    
059    float x = 0;
060    float y = 0;
061    AffineTransform transform = null;
062    String fontFamily;
063    float fontSize;
064    //List of strings and tspans containing the content of this node
065    LinkedList content = new LinkedList();
066    Shape textShape;
067    public static final int TXAN_START = 0;
068    public static final int TXAN_MIDDLE = 1;
069    public static final int TXAN_END = 2;
070    int textAnchor = TXAN_START;
071    public static final int TXST_NORMAL = 0;
072    public static final int TXST_ITALIC = 1;
073    public static final int TXST_OBLIQUE = 2;
074    int fontStyle;
075    public static final int TXWE_NORMAL = 0;
076    public static final int TXWE_BOLD = 1;
077    public static final int TXWE_BOLDER = 2;
078    public static final int TXWE_LIGHTER = 3;
079    public static final int TXWE_100 = 4;
080    public static final int TXWE_200 = 5;
081    public static final int TXWE_300 = 6;
082    public static final int TXWE_400 = 7;
083    public static final int TXWE_500 = 8;
084    public static final int TXWE_600 = 9;
085    public static final int TXWE_700 = 10;
086    public static final int TXWE_800 = 11;
087    public static final int TXWE_900 = 12;
088    int fontWeight;
089
090    /**
091     * Creates a new instance of Stop
092     */
093    public Text()
094    {
095    }
096
097    public String getTagName()
098    {
099        return TAG_NAME;
100    }
101
102    public void appendText(String text)
103    {
104        content.addLast(text);
105    }
106
107    public void appendTspan(Tspan tspan) throws SVGElementException
108    {
109        super.loaderAddChild(null, tspan);
110        content.addLast(tspan);
111    }
112
113    /**
114     * Discard cached information
115     */
116    public void rebuild() throws SVGException
117    {
118        build();
119    }
120
121    public java.util.List getContent()
122    {
123        return content;
124    }
125
126    /**
127     * Called after the start element but before the end element to indicate
128     * each child tag that has been processed
129     */
130    public void loaderAddChild(SVGLoaderHelper helper, SVGElement child) throws SVGElementException
131    {
132        super.loaderAddChild(helper, child);
133
134        content.addLast(child);
135    }
136
137    /**
138     * Called during load process to add text scanned within a tag
139     */
140    public void loaderAddText(SVGLoaderHelper helper, String text)
141    {
142        Matcher matchWs = Pattern.compile("\\s*").matcher(text);
143        if (!matchWs.matches())
144        {
145            content.addLast(text);
146        }
147    }
148
149    public void build() throws SVGException
150    {
151        super.build();
152
153        StyleAttribute sty = new StyleAttribute();
154
155        if (getPres(sty.setName("x")))
156        {
157            x = sty.getFloatValueWithUnits();
158        }
159
160        if (getPres(sty.setName("y")))
161        {
162            y = sty.getFloatValueWithUnits();
163        }
164
165        if (getStyle(sty.setName("font-family")))
166        {
167            fontFamily = sty.getStringValue();
168        } else
169        {
170            fontFamily = "Sans Serif";
171        }
172
173        if (getStyle(sty.setName("font-size")))
174        {
175            fontSize = sty.getFloatValueWithUnits();
176        } else
177        {
178            fontSize = 12f;
179        }
180
181        if (getStyle(sty.setName("font-style")))
182        {
183            String s = sty.getStringValue();
184            if ("normal".equals(s))
185            {
186                fontStyle = TXST_NORMAL;
187            } else if ("italic".equals(s))
188            {
189                fontStyle = TXST_ITALIC;
190            } else if ("oblique".equals(s))
191            {
192                fontStyle = TXST_OBLIQUE;
193            }
194        } else
195        {
196            fontStyle = TXST_NORMAL;
197        }
198
199        if (getStyle(sty.setName("font-weight")))
200        {
201            String s = sty.getStringValue();
202            if ("normal".equals(s))
203            {
204                fontWeight = TXWE_NORMAL;
205            } else if ("bold".equals(s))
206            {
207                fontWeight = TXWE_BOLD;
208            }
209        } else
210        {
211            fontWeight = TXWE_BOLD;
212        }
213
214        if (getStyle(sty.setName("text-anchor")))
215        {
216            String s = sty.getStringValue();
217            if (s.equals("middle"))
218            {
219                textAnchor = TXAN_MIDDLE;
220            } else if (s.equals("end"))
221            {
222                textAnchor = TXAN_END;
223            } else
224            {
225                textAnchor = TXAN_START;
226            }
227        } else
228        {
229            textAnchor = TXAN_START;
230        }
231
232        //text anchor
233        //text-decoration
234        //text-rendering
235
236        buildFont();
237    }
238
239    protected void buildFont() throws SVGException
240    {
241        int style;
242        switch (fontStyle)
243        {
244            case TXST_ITALIC:
245                style = java.awt.Font.ITALIC;
246                break;
247            default:
248                style = java.awt.Font.PLAIN;
249                break;
250        }
251
252        int weight;
253        switch (fontWeight)
254        {
255            case TXWE_BOLD:
256            case TXWE_BOLDER:
257                weight = java.awt.Font.BOLD;
258                break;
259            default:
260                weight = java.awt.Font.PLAIN;
261                break;
262        }
263
264        //Get font
265        Font font = diagram.getUniverse().getFont(fontFamily);
266        if (font == null)
267        {
268//            System.err.println("Could not load font");
269
270            java.awt.Font sysFont = new java.awt.Font(fontFamily, style | weight, (int) fontSize);
271            buildSysFont(sysFont);
272            return;
273        }
274
275//        font = new java.awt.Font(font.getFamily(), style | weight, font.getSize());
276
277//        Area textArea = new Area();
278        GeneralPath textPath = new GeneralPath();
279        textShape = textPath;
280
281        float cursorX = x, cursorY = y;
282
283        FontFace fontFace = font.getFontFace();
284        //int unitsPerEm = fontFace.getUnitsPerEm();
285        int ascent = fontFace.getAscent();
286        float fontScale = fontSize / (float) ascent;
287
288//        AffineTransform oldXform = g.getTransform();
289        AffineTransform xform = new AffineTransform();
290
291        for (Iterator it = content.iterator(); it.hasNext();)
292        {
293            Object obj = it.next();
294
295            if (obj instanceof String)
296            {
297                String text = (String) obj;
298                if (text != null)
299                {
300                    text = text.trim();
301                }
302
303                strokeWidthScalar = 1f / fontScale;
304
305                for (int i = 0; i < text.length(); i++)
306                {
307                    xform.setToIdentity();
308                    xform.setToTranslation(cursorX, cursorY);
309                    xform.scale(fontScale, fontScale);
310//                    g.transform(xform);
311
312                    String unicode = text.substring(i, i + 1);
313                    MissingGlyph glyph = font.getGlyph(unicode);
314
315                    Shape path = glyph.getPath();
316                    if (path != null)
317                    {
318                        path = xform.createTransformedShape(path);
319                        textPath.append(path, false);
320                    }
321//                    else glyph.render(g);
322
323                    cursorX += fontScale * glyph.getHorizAdvX();
324
325//                    g.setTransform(oldXform);
326                }
327
328                strokeWidthScalar = 1f;
329            } else if (obj instanceof Tspan)
330            {
331                Tspan tspan = (Tspan) obj;
332
333                xform.setToIdentity();
334                xform.setToTranslation(cursorX, cursorY);
335                xform.scale(fontScale, fontScale);
336//                tspan.setCursorX(cursorX);
337//                tspan.setCursorY(cursorY);
338
339                Shape tspanShape = tspan.getShape();
340                tspanShape = xform.createTransformedShape(tspanShape);
341                textPath.append(tspanShape, false);
342//                tspan.render(g);
343//                cursorX = tspan.getCursorX();
344//                cursorY = tspan.getCursorY();
345            }
346
347        }
348
349        switch (textAnchor)
350        {
351            case TXAN_MIDDLE:
352            {
353                AffineTransform at = new AffineTransform();
354                at.translate(-textPath.getBounds2D().getWidth() / 2, 0);
355                textPath.transform(at);
356                break;
357            }
358            case TXAN_END:
359            {
360                AffineTransform at = new AffineTransform();
361                at.translate(-textPath.getBounds2D().getWidth(), 0);
362                textPath.transform(at);
363                break;
364            }
365        }
366    }
367
368    private void buildSysFont(java.awt.Font font) throws SVGException
369    {
370        GeneralPath textPath = new GeneralPath();
371        textShape = textPath;
372
373        float cursorX = x, cursorY = y;
374
375//        FontMetrics fm = g.getFontMetrics(font);
376        FontRenderContext frc = new FontRenderContext(null, true, true);
377
378//        FontFace fontFace = font.getFontFace();
379        //int unitsPerEm = fontFace.getUnitsPerEm();
380//        int ascent = fm.getAscent();
381//        float fontScale = fontSize / (float)ascent;
382
383//        AffineTransform oldXform = g.getTransform();
384        AffineTransform xform = new AffineTransform();
385
386        for (Iterator it = content.iterator(); it.hasNext();)
387        {
388            Object obj = it.next();
389
390            if (obj instanceof String)
391            {
392                String text = (String) obj;
393
394                Shape textShape = font.createGlyphVector(frc, text).getOutline(cursorX, cursorY);
395                textPath.append(textShape, false);
396//                renderShape(g, textShape);
397//                g.drawString(text, cursorX, cursorY);
398
399                Rectangle2D rect = font.getStringBounds(text, frc);
400                cursorX += (float) rect.getWidth();
401            } else if (obj instanceof Tspan)
402            {
403                /*
404                 Tspan tspan = (Tspan)obj;
405                 
406                 xform.setToIdentity();
407                 xform.setToTranslation(cursorX, cursorY);
408                 
409                 Shape tspanShape = tspan.getShape();
410                 tspanShape = xform.createTransformedShape(tspanShape);
411                 textArea.add(new Area(tspanShape));
412                 
413                 cursorX += tspanShape.getBounds2D().getWidth();
414                 */
415
416
417                Tspan tspan = (Tspan) obj;
418                tspan.setCursorX(cursorX);
419                tspan.setCursorY(cursorY);
420                tspan.addShape(textPath);
421                cursorX = tspan.getCursorX();
422                cursorY = tspan.getCursorY();
423
424            }
425        }
426
427        switch (textAnchor)
428        {
429            case TXAN_MIDDLE:
430            {
431                AffineTransform at = new AffineTransform();
432                at.translate(-textPath.getBounds2D().getWidth() / 2, 0);
433                textPath.transform(at);
434                break;
435            }
436            case TXAN_END:
437            {
438                AffineTransform at = new AffineTransform();
439                at.translate(-textPath.getBounds2D().getWidth(), 0);
440                textPath.transform(at);
441                break;
442            }
443        }
444    }
445
446    public void render(Graphics2D g) throws SVGException
447    {
448        beginLayer(g);
449        renderShape(g, textShape);
450        finishLayer(g);
451    }
452
453    public Shape getShape()
454    {
455        return shapeToParent(textShape);
456    }
457
458    public Rectangle2D getBoundingBox() throws SVGException
459    {
460        return boundsToParent(includeStrokeInBounds(textShape.getBounds2D()));
461    }
462
463    /**
464     * Updates all attributes in this diagram associated with a time event. Ie,
465     * all attributes with track information.
466     *
467     * @return - true if this node has changed state as a result of the time
468     * update
469     */
470    public boolean updateTime(double curTime) throws SVGException
471    {
472//        if (trackManager.getNumTracks() == 0) return false;
473        boolean changeState = super.updateTime(curTime);
474
475        //Get current values for parameters
476        StyleAttribute sty = new StyleAttribute();
477        boolean shapeChange = false;
478
479        if (getPres(sty.setName("x")))
480        {
481            float newVal = sty.getFloatValueWithUnits();
482            if (newVal != x)
483            {
484                x = newVal;
485                shapeChange = true;
486            }
487        }
488
489        if (getPres(sty.setName("y")))
490        {
491            float newVal = sty.getFloatValueWithUnits();
492            if (newVal != y)
493            {
494                y = newVal;
495                shapeChange = true;
496            }
497        }
498
499        if (getPres(sty.setName("font-family")))
500        {
501            String newVal = sty.getStringValue();
502            if (!newVal.equals(fontFamily))
503            {
504                fontFamily = newVal;
505                shapeChange = true;
506            }
507        }
508
509        if (getPres(sty.setName("font-size")))
510        {
511            float newVal = sty.getFloatValueWithUnits();
512            if (newVal != fontSize)
513            {
514                fontSize = newVal;
515                shapeChange = true;
516            }
517        }
518
519
520        if (getStyle(sty.setName("font-style")))
521        {
522            String s = sty.getStringValue();
523            int newVal = fontStyle;
524            if ("normal".equals(s))
525            {
526                newVal = TXST_NORMAL;
527            } else if ("italic".equals(s))
528            {
529                newVal = TXST_ITALIC;
530            } else if ("oblique".equals(s))
531            {
532                newVal = TXST_OBLIQUE;
533            }
534            if (newVal != fontStyle)
535            {
536                fontStyle = newVal;
537                shapeChange = true;
538            }
539        }
540
541        if (getStyle(sty.setName("font-weight")))
542        {
543            String s = sty.getStringValue();
544            int newVal = fontWeight;
545            if ("normal".equals(s))
546            {
547                newVal = TXWE_NORMAL;
548            } else if ("bold".equals(s))
549            {
550                newVal = TXWE_BOLD;
551            }
552            if (newVal != fontWeight)
553            {
554                fontWeight = newVal;
555                shapeChange = true;
556            }
557        }
558
559        if (shapeChange)
560        {
561            build();
562//            buildFont();
563//            return true;
564        }
565
566        return changeState || shapeChange;
567    }
568}