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