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