/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2008 - 2009, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.display2d.style.j2d; import java.awt.Font; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Stroke; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.geom.AffineTransform; import java.awt.geom.FlatteningPathIterator; import java.awt.geom.GeneralPath; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; /** * Text Stroke special for OGC Symbology encoding. * * @author Johann Sorel (Geomatys) * @module */ public class TextStroke implements Stroke { private static final byte STATE_INITIAL_GAP = 0; private static final byte STATE_GAP = 1; private static final byte STATE_LABEL = 2; private final String text; private final Font font; private final boolean repeat; private final AffineTransform t = new AffineTransform(); private final float offset; private final float initialGap; private final float gap; private final Rectangle clip; public TextStroke(final String text, final Font font, final boolean repeat, final float offset, final float initialgap, final float gap, final Rectangle clip) { this.text = text; this.font = font; this.repeat = repeat; this.offset = offset; this.initialGap = initialgap; this.gap = gap; this.clip = clip; } @Override public Shape createStrokedShape(final Shape shape) { final FontRenderContext frc = new FontRenderContext(null, true, true); final GlyphVector glyphVector = font.createGlyphVector(frc, text); final GeneralPath result = new GeneralPath(); final float[] points = new float[6]; final int labelLength = glyphVector.getNumGlyphs(); //no label to paint if (labelLength == 0) return result; final float totalLabelLenght = (float) glyphVector.getVisualBounds().getWidth(); float remainingPathLength = measurePathLength(shape); //path is to short to paint label if( (initialGap+totalLabelLenght) > remainingPathLength){ return result; } final PathIterator it = new FlatteningPathIterator(shape.getPathIterator(null), 1); float moveX = 0, moveY = 0; float lastX = 0, lastY = 0; float thisX = 0, thisY = 0; int segmentType = 0; float next = 0; float remainingSegmentsize = 0; int currentChar = 0; //initialise values byte state = STATE_INITIAL_GAP; float initialGapToConsume = initialGap; float gapToConsume = gap; next = glyphVector.getGlyphMetrics(currentChar).getAdvance(); mainLoop : while(!it.isDone()){ //while we havent reach the end of the path iterator segmentType = it.currentSegment(points); switch(segmentType){ case PathIterator.SEG_MOVETO: moveX = lastX = points[0]; moveY = lastY = points[1]; result.moveTo(moveX, moveY); break; case PathIterator.SEG_CLOSE: points[0] = moveX; points[1] = moveY; // Fall into.... case PathIterator.SEG_LINETO: thisX = points[0]; thisY = points[1]; final float dx = thisX - lastX; final float dy = thisY - lastY; final float distance = (float) Math.sqrt(dx * dx + dy * dy); float segmentToConsume = distance + remainingSegmentsize; remainingSegmentsize = 0; remainingPathLength -= distance; segmentLoop : while(segmentToConsume>0){ //while the segment is not consume switch(state){ case STATE_INITIAL_GAP : //PASS THE INITIAL GAP-------------------------- if(segmentToConsume<initialGapToConsume){ //segment is completly consume by the initial gap initialGapToConsume -= segmentToConsume; segmentToConsume = 0; }else if(segmentToConsume==initialGapToConsume){ //segment is completly consume by the initial gap //and finish the initial gap initialGapToConsume = 0; segmentToConsume = 0; state = STATE_LABEL; }else{ //segment is bigger than the initial gap segmentToConsume -= initialGapToConsume; state = STATE_LABEL; } break; case STATE_GAP : //PASS A GAP------------------------------------ if(segmentToConsume<gapToConsume){ //segment is completly consume by the gap gapToConsume -= segmentToConsume; segmentToConsume = 0; }else if(segmentToConsume==gapToConsume){ //segment is completly consume by the gap //and finish the gap segmentToConsume = 0; state = STATE_LABEL; //restore gap value for next gap gapToConsume = gap; }else{ //segment is bigger than the gap segmentToConsume -= gapToConsume; state = STATE_LABEL; //restore gap value for next gap gapToConsume = gap; } break; case STATE_LABEL : //DRAW THE LABEL-------------------------------- final float angle = (float) Math.atan2(dy, dx); labelLoop : while(segmentToConsume >= next){ //check if we are in the clip area final float x = lastX + ((distance-segmentToConsume)/distance) * dx ; final float y = lastY + ((distance-segmentToConsume)/distance) * dy ; if(clip.contains(x, y)){ //we are in the clip area final Shape charGlyph = glyphVector.getGlyphOutline(currentChar); final Point2D p = glyphVector.getGlyphPosition(currentChar); final float px = (float) p.getX(); final float py = (float) p.getY(); t.setToTranslation(x+0, y); t.rotate(angle); t.translate(-px, -py); t.translate(0, -offset); result.append(t.createTransformedShape(charGlyph), false); } segmentToConsume -= next; currentChar++; if(currentChar>=labelLength){ //we reach the end of the String if(repeat && remainingPathLength > (gap+totalLabelLenght)){ //get back to string start //if there is enough space to draw another label currentChar = 0; //prepare to paint the gap state = STATE_GAP; continue segmentLoop; }else{ //we have finish, only one label painted break mainLoop; } }else{ //some more caracteres to paint next = glyphVector.getGlyphMetrics(currentChar).getAdvance(); } } //store the remaining segment size for next caractere //this is to avoid incoherent caractere spacing when several //to small segments are chained remainingSegmentsize = segmentToConsume; segmentToConsume = 0; break; } } lastX = thisX; lastY = thisY; break; } it.next(); } return result; } public float measurePathLength(final Shape shape) { final PathIterator it = new FlatteningPathIterator(shape.getPathIterator(null), 1); final float[] points = new float[6]; float moveX = 0, moveY = 0; float lastX = 0, lastY = 0; float thisX = 0, thisY = 0; int type = 0; float total = 0; while (!it.isDone()) { type = it.currentSegment(points); switch (type) { case PathIterator.SEG_MOVETO: moveX = lastX = points[0]; moveY = lastY = points[1]; break; case PathIterator.SEG_CLOSE: points[0] = moveX; points[1] = moveY; // Fall into.... case PathIterator.SEG_LINETO: thisX = points[0]; thisY = points[1]; final float dx = thisX - lastX; final float dy = thisY - lastY; total += (float) Math.sqrt(dx * dx + dy * dy); lastX = thisX; lastY = thisY; break; } it.next(); } return total; } }