001    /*
002     * Stop.java
003     *
004     *
005     *  The Salamander Project - 2D and 3D graphics libraries in Java
006     *  Copyright (C) 2004 Mark McKay
007     *
008     *  This library is free software; you can redistribute it and/or
009     *  modify it under the terms of the GNU Lesser General Public
010     *  License as published by the Free Software Foundation; either
011     *  version 2.1 of the License, or (at your option) any later version.
012     *
013     *  This library is distributed in the hope that it will be useful,
014     *  but WITHOUT ANY WARRANTY; without even the implied warranty of
015     *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
016     *  Lesser General Public License for more details.
017     *
018     *  You should have received a copy of the GNU Lesser General Public
019     *  License along with this library; if not, write to the Free Software
020     *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
021     *
022     *  Mark McKay can be contacted at mark@kitfox.com.  Salamander and other
023     *  projects can be found at http://www.kitfox.com
024     *
025     * Created on January 26, 2004, 1:56 AM
026     */
027    
028    package com.kitfox.svg;
029    
030    import com.kitfox.svg.xml.StyleAttribute;
031    import java.awt.*;
032    import java.awt.font.*;
033    import java.awt.geom.*;
034    import java.util.*;
035    import java.util.regex.*;
036    
037    //import org.apache.batik.ext.awt.geom.ExtendedGeneralPath;
038    
039    /**
040     * @author Mark McKay
041     * @author <a href="mailto:mark@kitfox.com">Mark McKay</a>
042     */
043    public class Text extends ShapeElement
044    {
045        
046        float x = 0;
047        float y = 0;
048        AffineTransform transform = null;
049        
050        String fontFamily;
051        float fontSize;
052        
053        //List of strings and tspans containing the content of this node
054        LinkedList content = new LinkedList();
055        
056        Shape textShape;
057        
058        public static final int TXAN_START = 0;
059        public static final int TXAN_MIDDLE = 1;
060        public static final int TXAN_END = 2;
061        int textAnchor = TXAN_START;
062        
063        public static final int TXST_NORMAL = 0;
064        public static final int TXST_ITALIC = 1;
065        public static final int TXST_OBLIQUE = 2;
066        int fontStyle;
067        
068        public static final int TXWE_NORMAL = 0;
069        public static final int TXWE_BOLD = 1;
070        public static final int TXWE_BOLDER = 2;
071        public static final int TXWE_LIGHTER = 3;
072        public static final int TXWE_100 = 4;
073        public static final int TXWE_200 = 5;
074        public static final int TXWE_300 = 6;
075        public static final int TXWE_400 = 7;
076        public static final int TXWE_500 = 8;
077        public static final int TXWE_600 = 9;
078        public static final int TXWE_700 = 10;
079        public static final int TXWE_800 = 11;
080        public static final int TXWE_900 = 12;
081        int fontWeight;
082        
083        /** Creates a new instance of Stop */
084        public Text()
085        {
086        }
087        
088        public void appendText(String text)
089        {
090            content.addLast(text);
091        }
092        
093        public void appendTspan(Tspan tspan) throws SVGElementException
094        {
095            super.loaderAddChild(null, tspan);
096            content.addLast(tspan);
097    //        tspan.setParent(this);
098        }
099        
100        /**
101         * Discard cached information
102         */
103        public void rebuild() throws SVGException
104        {
105            build();
106        }
107        
108        public java.util.List getContent()
109        {
110            return content;
111        }
112    /*
113        public void loaderStartElement(SVGLoaderHelper helper, Attributes attrs, SVGElement parent)
114        {
115                    //Load style string
116            super.loaderStartElement(helper, attrs, parent);
117     
118            String x = attrs.getValue("x");
119            String y = attrs.getValue("y");
120            //String transform = attrs.getValue("transform");
121     
122            this.x = XMLParseUtil.parseFloat(x);
123            this.y = XMLParseUtil.parseFloat(y);
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()) content.addLast(text);
144        }
145        
146        public void build() throws SVGException
147        {
148            super.build();
149            
150            StyleAttribute sty = new StyleAttribute();
151            
152            if (getPres(sty.setName("x"))) x = sty.getFloatValueWithUnits();
153            
154            if (getPres(sty.setName("y"))) y = sty.getFloatValueWithUnits();
155            
156            if (getStyle(sty.setName("font-family"))) fontFamily = sty.getStringValue();
157            else fontFamily = "Sans Serif";
158            
159            if (getStyle(sty.setName("font-size"))) fontSize = sty.getFloatValueWithUnits();
160            else fontSize = 12f;
161            
162            if (getStyle(sty.setName("font-style")))
163            {
164                String s = sty.getStringValue();
165                if ("normal".equals(s))
166                {
167                    fontStyle = TXST_NORMAL;
168                }
169                else if ("italic".equals(s))
170                {
171                    fontStyle = TXST_ITALIC;
172                }
173                else if ("oblique".equals(s))
174                {
175                    fontStyle = TXST_OBLIQUE;
176                }
177            }
178            else fontStyle = TXST_NORMAL;
179            
180            if (getStyle(sty.setName("font-weight")))
181            {
182                String s = sty.getStringValue();
183                if ("normal".equals(s))
184                {
185                    fontWeight = TXWE_NORMAL;
186                }
187                else if ("bold".equals(s))
188                {
189                    fontWeight = TXWE_BOLD;
190                }
191            }
192            else fontWeight = TXWE_BOLD;
193            
194            if (getStyle(sty.setName("text-anchor")))
195            {
196                String s = sty.getStringValue();
197                if (s.equals("middle")) textAnchor = TXAN_MIDDLE;
198                else if (s.equals("end")) textAnchor = TXAN_END;
199                else textAnchor = TXAN_START;
200            }
201            else textAnchor = TXAN_START;
202            
203            //text anchor
204            //text-decoration
205            //text-rendering
206            
207            buildFont();
208        }
209        
210        protected void buildFont() throws SVGException
211        {
212            int style;
213            switch (fontStyle)
214            {
215                case TXST_ITALIC:
216                    style = java.awt.Font.ITALIC;
217                    break;
218                default:
219                    style = java.awt.Font.PLAIN;
220                    break;
221            }
222    
223            int weight;
224            switch (fontWeight)
225            {
226                case TXWE_BOLD:
227                case TXWE_BOLDER:
228                    weight = java.awt.Font.BOLD;
229                    break;
230                default:
231                    weight = java.awt.Font.PLAIN;
232                    break;
233            }
234                
235            //Get font
236            Font font = diagram.getUniverse().getFont(fontFamily);
237            if (font == null)
238            {
239    //            System.err.println("Could not load font");
240                
241                java.awt.Font sysFont = new java.awt.Font(fontFamily, style | weight, (int)fontSize);
242                buildSysFont(sysFont);
243                return;
244            }
245            
246    //        font = new java.awt.Font(font.getFamily(), style | weight, font.getSize());
247            
248    //        Area textArea = new Area();
249            GeneralPath textPath = new GeneralPath();
250            textShape = textPath;
251            
252            float cursorX = x, cursorY = y;
253            
254            FontFace fontFace = font.getFontFace();
255            //int unitsPerEm = fontFace.getUnitsPerEm();
256            int ascent = fontFace.getAscent();
257            float fontScale = fontSize / (float)ascent;
258            
259    //        AffineTransform oldXform = g.getTransform();
260            AffineTransform xform = new AffineTransform();
261            
262            for (Iterator it = content.iterator(); it.hasNext();)
263            {
264                Object obj = it.next();
265                
266                if (obj instanceof String)
267                {
268                    String text = (String)obj;
269                    
270                    strokeWidthScalar = 1f / fontScale;
271                    
272                    for (int i = 0; i < text.length(); i++)
273                    {
274                        xform.setToIdentity();
275                        xform.setToTranslation(cursorX, cursorY);
276                        xform.scale(fontScale, fontScale);
277    //                    g.transform(xform);
278                        
279                        String unicode = text.substring(i, i + 1);
280                        MissingGlyph glyph = font.getGlyph(unicode);
281                        
282                        Shape path = glyph.getPath();
283                        if (path != null)
284                        {
285                            path = xform.createTransformedShape(path);
286                            textPath.append(path, false);
287                        }
288    //                    else glyph.render(g);
289                        
290                        cursorX += fontScale * glyph.getHorizAdvX();
291                        
292    //                    g.setTransform(oldXform);
293                    }
294                    
295                    strokeWidthScalar = 1f;
296                }
297                else if (obj instanceof Tspan)
298                {
299                    Tspan tspan = (Tspan)obj;
300                    
301                    xform.setToIdentity();
302                    xform.setToTranslation(cursorX, cursorY);
303                    xform.scale(fontScale, fontScale);
304    //                tspan.setCursorX(cursorX);
305    //                tspan.setCursorY(cursorY);
306                    
307                    Shape tspanShape = tspan.getShape();
308                    tspanShape = xform.createTransformedShape(tspanShape);
309                    textPath.append(tspanShape, false);
310    //                tspan.render(g);
311    //                cursorX = tspan.getCursorX();
312    //                cursorY = tspan.getCursorY();
313                }
314                
315            }
316            
317            switch (textAnchor)
318            {
319                case TXAN_MIDDLE:
320                {
321                    AffineTransform at = new AffineTransform();
322                    at.translate(-textPath.getBounds2D().getWidth() / 2, 0);
323                    textPath.transform(at);
324                    break;
325                }
326                case TXAN_END:
327                {
328                    AffineTransform at = new AffineTransform();
329                    at.translate(-textPath.getBounds2D().getWidth(), 0);
330                    textPath.transform(at);
331                    break;
332                }
333            }
334        }
335        
336        private void buildSysFont(java.awt.Font font) throws SVGException
337        {
338            GeneralPath textPath = new GeneralPath();
339            textShape = textPath;
340            
341            float cursorX = x, cursorY = y;
342            
343    //        FontMetrics fm = g.getFontMetrics(font);
344            FontRenderContext frc = new FontRenderContext(null, true, true);
345            
346    //        FontFace fontFace = font.getFontFace();
347            //int unitsPerEm = fontFace.getUnitsPerEm();
348    //        int ascent = fm.getAscent();
349    //        float fontScale = fontSize / (float)ascent;
350            
351    //        AffineTransform oldXform = g.getTransform();
352            AffineTransform xform = new AffineTransform();
353            
354            for (Iterator it = content.iterator(); it.hasNext();)
355            {
356                Object obj = it.next();
357                
358                if (obj instanceof String)
359                {
360                    String text = (String)obj;
361                    
362                    Shape textShape = font.createGlyphVector(frc, text).getOutline(cursorX, cursorY);
363                    textPath.append(textShape, false);
364    //                renderShape(g, textShape);
365    //                g.drawString(text, cursorX, cursorY);
366                    
367                    Rectangle2D rect = font.getStringBounds(text, frc);
368                    cursorX += (float)rect.getWidth();
369                }
370                else if (obj instanceof Tspan)
371                {
372                    /*
373                    Tspan tspan = (Tspan)obj;
374                     
375                    xform.setToIdentity();
376                    xform.setToTranslation(cursorX, cursorY);
377                     
378                    Shape tspanShape = tspan.getShape();
379                    tspanShape = xform.createTransformedShape(tspanShape);
380                    textArea.add(new Area(tspanShape));
381                     
382                    cursorX += tspanShape.getBounds2D().getWidth();
383                     */
384                    
385                    
386                    Tspan tspan = (Tspan)obj;
387                    tspan.setCursorX(cursorX);
388                    tspan.setCursorY(cursorY);
389                    tspan.addShape(textPath);
390                    cursorX = tspan.getCursorX();
391                    cursorY = tspan.getCursorY();
392                    
393                }
394            }
395            
396            switch (textAnchor)
397            {
398                case TXAN_MIDDLE:
399                {
400                    AffineTransform at = new AffineTransform();
401                    at.translate(-textPath.getBounds2D().getWidth() / 2, 0);
402                    textPath.transform(at);
403                    break;
404                }
405                case TXAN_END:
406                {
407                    AffineTransform at = new AffineTransform();
408                    at.translate(-textPath.getBounds2D().getWidth(), 0);
409                    textPath.transform(at);
410                    break;
411                }
412            }
413        }
414        
415        
416        public void render(Graphics2D g) throws SVGException
417        {
418            beginLayer(g);
419            renderShape(g, textShape);
420            finishLayer(g);
421        }
422        
423        public Shape getShape()
424        {
425            return shapeToParent(textShape);
426        }
427        
428        public Rectangle2D getBoundingBox() throws SVGException
429        {
430            return boundsToParent(includeStrokeInBounds(textShape.getBounds2D()));
431        }
432        
433        /**
434         * Updates all attributes in this diagram associated with a time event.
435         * Ie, all attributes with track information.
436         * @return - true if this node has changed state as a result of the time
437         * update
438         */
439        public boolean updateTime(double curTime) throws SVGException
440        {
441    //        if (trackManager.getNumTracks() == 0) return false;
442            boolean changeState = super.updateTime(curTime);
443            
444            //Get current values for parameters
445            StyleAttribute sty = new StyleAttribute();
446            boolean shapeChange = false;
447            
448            if (getPres(sty.setName("x")))
449            {
450                float newVal = sty.getFloatValueWithUnits();
451                if (newVal != x)
452                {
453                    x = newVal;
454                    shapeChange = true;
455                }
456            }
457            
458            if (getPres(sty.setName("y")))
459            {
460                float newVal = sty.getFloatValueWithUnits();
461                if (newVal != y)
462                {
463                    y = newVal;
464                    shapeChange = true;
465                }
466            }
467            
468            if (getPres(sty.setName("font-family")))
469            {
470                String newVal = sty.getStringValue();
471                if (!newVal.equals(fontFamily))
472                {
473                    fontFamily = newVal;
474                    shapeChange = true;
475                }
476            }
477            
478            if (getPres(sty.setName("font-size")))
479            {
480                float newVal = sty.getFloatValueWithUnits();
481                if (newVal != fontSize)
482                {
483                    fontSize = newVal;
484                    shapeChange = true;
485                }
486            }
487            
488            
489            if (getStyle(sty.setName("font-style")))
490            {
491                String s = sty.getStringValue();
492                int newVal = fontStyle;
493                if ("normal".equals(s))
494                {
495                    newVal = TXST_NORMAL;
496                }
497                else if ("italic".equals(s))
498                {
499                    newVal = TXST_ITALIC;
500                }
501                else if ("oblique".equals(s))
502                {
503                    newVal = TXST_OBLIQUE;
504                }
505                if (newVal != fontStyle)
506                {
507                    fontStyle = newVal;
508                    shapeChange = true;
509                }
510            }
511            
512            if (getStyle(sty.setName("font-weight")))
513            {
514                String s = sty.getStringValue();
515                int newVal = fontWeight;
516                if ("normal".equals(s))
517                {
518                    newVal = TXWE_NORMAL;
519                }
520                else if ("bold".equals(s))
521                {
522                    newVal = TXWE_BOLD;
523                }
524                if (newVal != fontWeight)
525                {
526                    fontWeight = newVal;
527                    shapeChange = true;
528                }
529            }
530            
531            if (shapeChange)
532            {
533                build();
534    //            buildFont();
535    //            return true;
536            }
537            
538            return changeState || shapeChange;
539        }
540    }