Statistics
| Revision:

svn-gvsig-desktop / trunk / org.gvsig.desktop / org.gvsig.desktop.library / org.gvsig.symbology / org.gvsig.symbology.lib / org.gvsig.symbology.lib.impl / src / main / java / org / gvsig / symbology / fmap / mapcontext / rendering / legend / styling / TextPath.java @ 43156

History | View | Annotate | Download (16.1 KB)

1
/**
2
 * gvSIG. Desktop Geographic Information System.
3
 *
4
 * Copyright (C) 2007-2013 gvSIG Association.
5
 *
6
 * This program is free software; you can redistribute it and/or
7
 * modify it under the terms of the GNU General Public License
8
 * as published by the Free Software Foundation; either version 3
9
 * of the License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program; if not, write to the Free Software
18
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19
 * MA  02110-1301, USA.
20
 *
21
 * For any additional information, do not hesitate to contact us
22
 * at info AT gvsig.com, or visit our website www.gvsig.com.
23
 */
24
package org.gvsig.symbology.fmap.mapcontext.rendering.legend.styling;
25

    
26
import java.awt.Font;
27
import java.awt.Graphics2D;
28
import java.awt.font.FontRenderContext;
29
import java.awt.font.GlyphVector;
30
import java.awt.geom.Point2D;
31

    
32
import org.apache.batik.ext.awt.geom.DefaultPathLength;
33
import org.gvsig.fmap.geom.Geometry;
34
import org.gvsig.fmap.geom.Geometry.SUBTYPES;
35
import org.gvsig.fmap.geom.GeometryLocator;
36
import org.gvsig.fmap.geom.GeometryManager;
37
import org.gvsig.fmap.geom.exception.CreateGeometryException;
38
import org.gvsig.fmap.geom.primitive.GeneralPathX;
39
import org.gvsig.fmap.mapcontext.rendering.symbols.ITextSymbol;
40
import org.gvsig.i18n.Messages;
41
import org.slf4j.Logger;
42
import org.slf4j.LoggerFactory;
43

    
44
import com.vividsolutions.jts.algorithm.Angle;
45
import org.gvsig.symbology.PathLength;
46
/**
47
 * <p>Class that represents baseline of a string and allows the baseline to
48
 * be composed as contiguous segments with distinct slope each.<br></p>
49
 *
50
 * <p>Once a TextPath is created for a string it is possible to know where
51
 * the character at a determined position in the string is placed and
52
 * rotated.<br></p>
53
 *
54
 */
55
public class TextPath {
56
        private static final GeometryManager geomManager = GeometryLocator.getGeometryManager();
57
        private static final Logger logger = LoggerFactory.getLogger(GeometryManager.class);
58

    
59
        public static final int NO_POS = Integer.MIN_VALUE;
60
        /**
61
         * Don't set a concrete word spacing. The word is separated using the normal
62
         * width of the separator glyph.
63
         */
64
        public static final int DEFAULT_WORD_SPACING = Integer.MIN_VALUE;
65

    
66
//        private char[] text;
67
        /**
68
         * An array which contains the calculated positions for the glyphs
69
         * Each row represents a glyph, and it contains the X coord, the Y coord, and the rotation angle
70
         */
71
        private double[][] posList;
72
//        private int alignment;
73
        private float characterSpacing;
74
//        private boolean kerning;
75
        private float wordSpacing;
76
        private float margin;
77
        private boolean rightToLeft;
78
        private int numGlyphs = 0;
79
        private float characterWidth;
80
        private char[] wordSeparators = {' '}; // in the future, separators might be provided as parameter
81

    
82
        /**
83
         * <p>Creates a new instance of TextPath with the current graphics
84
         * context.<br></p>
85
         *
86
         * <p>Given a <b>Graphics2D</b>, TextPath can know which Font and FontRenderContext
87
         * is in use. So, it can calculate the position and rotation of each
88
         * character in <b>char[] text</b> based in the path defined by the
89
         * <b>FShape path</b> argument.</p>
90
         * @param g, Graphics2D
91
         * @param path, FShape
92
         * @param text, char[]
93
         */
94
        public TextPath(Graphics2D g, Geometry path, char[] text, Font font,
95
                        float characterSpacing, float characterWidth, boolean kerning,
96
                        float leading, int alignment, float wordSpacing, float margin,
97
                        boolean rightToLeft) {
98
//                this.text = text;
99
                if (alignment == ITextSymbol.SYMBOL_STYLE_ALIGNMENT_LEFT ||
100
                                alignment == ITextSymbol.SYMBOL_STYLE_ALIGNMENT_RIGHT
101
                                ||
102
                                alignment == ITextSymbol.SYMBOL_STYLE_ALIGNMENT_CENTERED ||
103
                                alignment == ITextSymbol.SYMBOL_STYLE_ALIGNMENT_JUSTIFY) {
104
//                        this.alignment = alignment;
105
                } else {
106
                        throw new IllegalArgumentException(
107
                                        Messages.getText("invalid_value_for") + ": " +
108
                                        Messages.getText("alignment")+" ( "+alignment+")");
109
                }
110
                this.characterWidth = characterWidth;
111
                this.characterSpacing = characterSpacing;
112
//                this.kerning = kerning;
113
                this.wordSpacing = wordSpacing;
114
                this.margin = margin;
115
                this.rightToLeft = rightToLeft;
116

    
117
                FontRenderContext frc = g.getFontRenderContext();
118
                /* java 6 code
119
                 * TODO keep this!!
120
                if (kerning) {
121
                        HashMap<TextAttribute, Object> attrs = new HashMap<TextAttribute, Object>();
122
                        attrs.put(TextAttribute.KERNING , TextAttribute.KERNING_ON);
123
                }
124
                 */
125
                GlyphVector gv = font.createGlyphVector(frc, text);
126

    
127
                PathLength pl;
128
                try {
129
                        pl = new DefaultPathLength(softenShape(path, gv).getShape());
130
                        if (alignment==ITextSymbol.SYMBOL_STYLE_ALIGNMENT_RIGHT) {
131
                                posList = computeAtRight(gv, pl, text);
132
                        }
133
                        else if (alignment==ITextSymbol.SYMBOL_STYLE_ALIGNMENT_CENTERED) {
134
                                computeAtMiddle(frc, text, font, pl);
135
                        }
136
                        else {
137
                                posList = computeAtLeft(gv, pl, text);
138
                        }
139
                } catch (CreateGeometryException e) {
140
                        logger.error("Error creating a curve", e);
141
                }                
142
        }
143

    
144
        protected Geometry softenShape(Geometry shape, GlyphVector gv) throws CreateGeometryException {
145

    
146
                float interval = (float) gv.getVisualBounds().getWidth()/(gv.getNumGlyphs()*3);
147

    
148
                PathLength pl = new DefaultPathLength(shape.getShape());
149
                if (pl.lengthOfPath()==0.0f) {
150
                        return shape; 
151
                }
152
                
153
                GeneralPathX correctedPath = new GeneralPathX();
154
                int controlPoints = 16;
155
                double[][] points = new double[controlPoints][2];
156
                double prevX, prevY;
157
                double xsum=0, ysum=0;
158
                int nextPos = 0;
159
                boolean bufferComplete = false;
160
                boolean movedTo = false;
161
                for (float curPos = 0; curPos<pl.lengthOfPath(); curPos = curPos+interval) {
162
                        prevX = points[nextPos][0];
163
                        prevY = points[nextPos][1];
164
                        Point2D point =pl.pointAtLength(curPos);
165
                        if (!movedTo) {
166
                                correctedPath.moveTo(point.getX(), point.getY());
167
                                movedTo = true;
168
                        }
169

    
170
                        points[nextPos][0] = point.getX();
171
                        points[nextPos][1] = point.getY();
172

    
173
                        if (!bufferComplete) {
174
                                xsum += points[nextPos][0];
175
                                ysum += points[nextPos][1];
176
                                nextPos++;
177
                                if (nextPos==controlPoints) {
178
                                        nextPos = 0;
179
                                        bufferComplete = true;
180

    
181

    
182
                                        /**
183
                                         * calculate the beginning of the line
184
                                         */
185
                                        // this will be the first interpolated point
186
                                        double auxX2 = xsum/controlPoints;
187
                                        double auxY2 = ysum/controlPoints;
188

    
189
                                        for (int i=1; i<controlPoints/2-1; i++) {
190
                                                // calculate the points from the origin of the geometry to the first interpolated point
191
                                                double auxX = (points[0][0]+points[i][0]+auxX2)/3;
192
                                                double auxY = (points[0][1]+points[i][1]+auxY2)/3;
193
                                                correctedPath.lineTo(auxX, auxY);
194
                                        }
195
                                        correctedPath.lineTo(auxX2, auxY2);
196
                                }
197
                        }
198
                        else {
199

    
200
                                xsum = xsum - prevX + points[nextPos][0];
201
                                ysum = ysum - prevY + points[nextPos][1];
202
                                if (!movedTo) {
203
                                        correctedPath.moveTo(xsum/controlPoints, ysum/controlPoints);
204
                                        movedTo = true;
205
                                }
206
                                else {
207
                                        correctedPath.lineTo(xsum/controlPoints, ysum/controlPoints);
208
                                }
209

    
210
                                nextPos = (nextPos+1)%controlPoints;
211
                        }
212
                }
213
                Point2D endPoint = pl.pointAtLength(pl.lengthOfPath());
214
                // last point in the geom
215
                double endPointX = endPoint.getX();
216
                double endPointY = endPoint.getY();
217

    
218
                if (bufferComplete) {
219
                        /**
220
                         * calculate the points from the last interpolated point to the end of the geometry
221
                         */
222

    
223
                        // last interpolated point
224
                        double auxX2 = xsum/controlPoints;
225
                        double auxY2 = ysum/controlPoints;
226
                        nextPos = (nextPos+(controlPoints/2))%controlPoints;
227
                        for (int i=0; i<controlPoints/2-1; i++) {
228
                                // calculate the points from the last interpolated point to the end of the geometry
229
                                double auxX = (auxX2+points[nextPos][0]+endPointX)/3;
230
                                double auxY = (auxY2+points[nextPos][1]+endPointY)/3;
231
                                correctedPath.lineTo(auxX, auxY);
232
                                nextPos = (nextPos+1)%controlPoints;
233
                        }
234
                }
235
                correctedPath.lineTo(endPointX, endPointY);
236

    
237
                return geomManager.createCurve(new GeneralPathX(
238
                                correctedPath.getPathIterator(null)), SUBTYPES.GEOM2D);
239
        }
240

    
241
        /**
242
         * Initializes the position vector.
243
         * @param g
244
         * @param path
245
         */
246
        private double[][] computeAtRight(GlyphVector gv, PathLength pl, char[] text) {
247
                numGlyphs = gv.getNumGlyphs();
248
                double[][] pos = new double[numGlyphs][3];
249
                float[] charAnchors = new float[numGlyphs];
250

    
251
                /**
252
                 * Compute glyph positions using linear distances
253
                 */
254
                float lengthOfPath = pl.lengthOfPath();
255
                // char distance from the right side
256
                float charDistance = lengthOfPath-margin;
257
                int glyphsConsumed = numGlyphs-1;
258
                float previousAngle = 0.0f;
259
                float angle = 0.0f;
260
                boolean correction = true;
261
                float charWidth = characterWidth;
262
                for (int i = numGlyphs-1; i>=0; i--) {
263
                        if (correction && charDistance>=0) {
264
                                previousAngle = angle;
265
                                angle = pl.angleAtLength(charDistance);
266
                                if (i<numGlyphs-1) {
267
                                        // correct distance according to angle between current and previous glyph
268
                                        int turn = Angle.getTurn(previousAngle, angle);
269
                                        if (turn==1) {  // if turn is positive => increase distance
270
                                                float auxDistance = charDistance - (float)(charWidth*2.5f*Angle.diff(previousAngle, angle)/Math.PI);
271
                                                float auxAngle = pl.angleAtLength(auxDistance);
272
                                                if (Angle.getTurn(previousAngle, auxAngle)==1) { // ensure new position also has positive turn
273
                                                        charDistance = auxDistance;
274
                                                        angle = auxAngle;
275
                                                }
276
                                        }
277
                                        else if (turn==-1) { // if turn is negative => decrease distance
278
                                                float auxDistance = charDistance + (float)(charWidth*0.9f*Angle.diff(previousAngle, angle)/Math.PI);
279
                                                float auxAngle = pl.angleAtLength(auxDistance);
280
                                                if (Angle.getTurn(previousAngle, auxAngle)==-1) { // ensure new position also has negative turn
281
                                                        charDistance = auxDistance;
282
                                                        angle = auxAngle;
283
                                                }
284
                                        }
285
                                }
286
                        }
287

    
288
                        if (wordSpacing!=DEFAULT_WORD_SPACING
289
                                        && isWordSeparator(text[gv.getGlyphCharIndex(glyphsConsumed)], wordSeparators)) {
290
                                charWidth = wordSpacing;
291
                        }
292
                        else {
293
                                charWidth = Math.max(gv.getGlyphMetrics(glyphsConsumed).getAdvance(), characterWidth);
294

    
295
                        }
296
                        charDistance -= charWidth;
297
                        charAnchors[glyphsConsumed] = charDistance;
298
                        charDistance -= characterSpacing;
299
                        glyphsConsumed--;
300
                }
301

    
302
                /**
303
                 * Calculate 2D positions for the glyphs from the calculated linear distances
304
                 */
305
                for (int i = numGlyphs-1; i>=0; i--) {
306
                        float anchor = (rightToLeft) ? charAnchors[charAnchors.length-1-i] : charAnchors[i];
307
                        Point2D p = pl.pointAtLength( anchor );
308
                        if (p == null) {
309
                                if (i<numGlyphs-1) { // place in a straight line the glyphs that don't fit in the shape
310
                                        pos[i][0] = pos[i+1][0] + (charAnchors[i]-charAnchors[i+1])*Math.cos(pos[i+1][2]);
311
                                        pos[i][1] = pos[i+1][1] + (charAnchors[i]-charAnchors[i+1])*Math.sin(pos[i+1][2]);
312
                                        pos[i][2] = pos[i+1][2];
313
                                } else {
314
                                        pos[i][0] = NO_POS;
315
                                        pos[i][1] = NO_POS;
316
                                }
317
                                continue;
318
                        }
319
                        pos[i][0] = p.getX();
320
                        pos[i][1] = p.getY();
321
                        pos[i][2] = pl.angleAtLength( anchor );
322
                }
323
                return pos;
324
        }
325

    
326
        /**
327
         * Initializes the position vector.
328
         * @param g
329
         * @param path
330
         */
331
        private double[][] computeAtLeft(GlyphVector gv, PathLength pl, char[] text) {
332
                numGlyphs = gv.getNumGlyphs();
333
                double[][] pos = new double[numGlyphs][3];
334
                float[] charAnchors = new float[numGlyphs];
335
                float[] charWidths = new float[numGlyphs];
336

    
337
                /**
338
                 * Compute glyph positions using linear distances
339
                 */
340
                float lengthOfPath = pl.lengthOfPath();
341
                float charDistance = margin;
342
                int glyphsConsumed = 0;
343
                float previousAngle = 0.0f;
344
                float angle = 0.0f;
345
                boolean correction = true;
346
                float charWidth = characterWidth;
347
                for (int i = 0; i < gv.getNumGlyphs(); i++) {
348

    
349
                        if (correction && charDistance<=lengthOfPath) {
350
                                previousAngle = angle;
351
                                angle = pl.angleAtLength(charDistance);
352
                                if (i>0) {
353
                                        // correct distance according to angle between current and previous glyph
354
                                        int turn = Angle.getTurn(previousAngle, angle);
355
                                        if (turn==1) {  // if turn is positive => decrease distance
356
                                                float auxDistance = charDistance - (float)(charWidth*0.9*Angle.diff(previousAngle, angle)/Math.PI);
357
                                                float auxAngle = pl.angleAtLength(auxDistance);
358
                                                if (Angle.getTurn(previousAngle, auxAngle)==1) { // ensure new position also has positive turn
359
                                                        charDistance = auxDistance;
360
                                                        angle = auxAngle;
361
                                                }
362
                                        }
363
                                        else if (turn == -1){ // if turn is negative => increase distance
364

    
365
                                                float auxDistance = charDistance + (float)(charWidth*2.5*Angle.diff(previousAngle, angle)/Math.PI);
366
                                                float auxAngle = pl.angleAtLength(auxDistance);
367
                                                if (Angle.getTurn(previousAngle, auxAngle)==-1) { // ensure new position also has negative turn
368
                                                        charDistance = auxDistance;
369
                                                        angle = auxAngle;
370
                                                }
371
                                        }
372
                                }
373
                        }
374
                        if (wordSpacing!=DEFAULT_WORD_SPACING
375
                                        && isWordSeparator(text[gv.getGlyphCharIndex(glyphsConsumed)], wordSeparators)) {
376
                                // use defined wordspacing
377
                                charWidth = wordSpacing;
378
                        }
379
                        else {
380
                                charWidth = Math.max(gv.getGlyphMetrics(glyphsConsumed).getAdvance(), characterWidth);
381

    
382
                        }
383
                        charWidths[glyphsConsumed] = charWidth;
384
                        charAnchors[glyphsConsumed] = charDistance;
385
                        charDistance += charWidth;
386
                        charDistance += characterSpacing;
387
                        glyphsConsumed++;
388
                }
389

    
390
                /**
391
                 * Calculate 2D positions for the glyphs from the calculated linear distances
392
                 */
393
                for (int i = 0; i < charAnchors.length; i++) {
394
                        float anchor = (rightToLeft) ? charAnchors[charAnchors.length-1-i] : charAnchors[i];
395
                        Point2D p = pl.pointAtLength( anchor );
396
                        if (p == null) {
397
                                if (i>0) { // place in a straight line the glyphs that don't fit in the shape
398
                                        pos[i][0] = pos[i-1][0] + (charAnchors[i]-charAnchors[i-1])*Math.cos(pos[i-1][2]);
399
                                        pos[i][1] = pos[i-1][1] + (charAnchors[i]-charAnchors[i-1])*Math.sin(pos[i-1][2]);
400
                                        pos[i][2] = pos[i-1][2];
401
                                } else {
402
                                        pos[i][0] = NO_POS;
403
                                        pos[i][1] = NO_POS;
404
                                }
405
                                continue;
406
                        }
407
                        pos[i][2] = pl.angleAtLength( anchor );
408
                        //                        pos[i][0] = p.getX() - charWidths[i]*Math.cos(pos[i][2]);
409
                        //                        pos[i][1] = p.getY() - charWidths[i]*Math.sin(pos[i][2]);
410
                        pos[i][0] = p.getX();
411
                        pos[i][1] = p.getY();
412
                }
413
                return pos;
414
        }
415

    
416

    
417
        /**
418
         * Initializes the position vector.
419
         * @param g
420
         * @param path
421
         */
422
        private void computeAtMiddle(FontRenderContext frc, char[] text, Font font, PathLength pl) {
423
                if (text.length==0) {
424
                        return; // nothing to compute if text length is 0
425
                }
426
                int middleChar = (text.length-1)/2;
427
                char[] text1 = new char[middleChar+1];
428
                char[] text2 = new char[text.length-text1.length];
429
                System.arraycopy(text, 0, text1, 0, text1.length);
430
                System.arraycopy(text, text1.length,  text2, 0, text2.length);
431

    
432
                float halfLength = pl.lengthOfPath()/2.0f;
433
                margin = halfLength;
434
                GlyphVector gv = font.createGlyphVector(frc, text1);
435
                double[][] pos1 = computeAtRight(gv, pl, text1);
436
                int glyphCount = numGlyphs;
437
                gv = font.createGlyphVector(frc, text2);
438
                margin = halfLength + characterSpacing;
439
                double[][] pos2 = computeAtLeft(gv, pl, text2);
440
                numGlyphs += glyphCount;
441
                posList = new double[pos1.length+pos2.length][3];
442
                System.arraycopy(pos1, 0, posList, 0, pos1.length);
443
                System.arraycopy(pos2, 0, posList, pos1.length, pos2.length);
444
        }
445

    
446

    
447
        /**
448
         * <p>Returns the placement of the next character to draw and the corresponding
449
         * rotation in a double array of three elements with this order:</p><br>
450
         *
451
         * <p><b>double[0]</b> Position in X in the screen</p>
452
         * <p><b>double[1]</b> Position in Y in the screen</p>
453
         * <p><b>double[2]</b> Angle of the character.</p>
454
         * @return
455
         */
456
        public double[] nextPosForGlyph(int glyphIndex) {
457
                return posList[glyphIndex];
458
        }
459

    
460
        public int getGlyphCount() {
461
                return numGlyphs;
462
        }
463

    
464
        protected static boolean isWordSeparator(char c, char[] wordSeparators) {
465
                char separator;
466
                for (int i = 0; i < wordSeparators.length; i++) {
467
                        separator = wordSeparators[i];
468
                        if (c==separator) {
469
                                return true;
470
                        }
471
                }
472
                return false;
473
        }
474

    
475
}