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