source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/styleelement/placement/OnLineStrategy.java@ 11814

Last change on this file since 11814 was 11814, checked in by Don-vip, 7 years ago

sonar - squid:UnusedPrivateMethod

File size: 12.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint.styleelement.placement;
3
4import java.awt.font.GlyphVector;
5import java.awt.geom.AffineTransform;
6import java.awt.geom.Rectangle2D;
7import java.util.ArrayList;
8import java.util.Collections;
9import java.util.Comparator;
10import java.util.Iterator;
11import java.util.List;
12import java.util.Optional;
13import java.util.stream.IntStream;
14
15import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
16import org.openstreetmap.josm.gui.draw.MapViewPath;
17import org.openstreetmap.josm.gui.draw.MapViewPath.PathSegmentConsumer;
18import org.openstreetmap.josm.gui.draw.MapViewPositionAndRotation;
19
20/**
21 * Places the label onto the line.
22 *
23 * @author Michael Zangl
24 * @since 11722
25 * @since 11748 moved to own file
26 */
27public class OnLineStrategy implements PositionForAreaStrategy {
28 /**
29 * An instance of this class.
30 */
31 public static final OnLineStrategy INSTANCE = new OnLineStrategy(0);
32
33 private final double yOffset;
34
35 /**
36 * Create a new strategy that places the text on the line.
37 * @param yOffset The offset sidewards to the line.
38 */
39 public OnLineStrategy(double yOffset) {
40 this.yOffset = yOffset;
41 }
42
43 @Override
44 public MapViewPositionAndRotation findLabelPlacement(MapViewPath path, Rectangle2D nb) {
45 return findOptimalWayPosition(nb, path).map(best -> {
46 MapViewPoint center = best.start.interpolate(best.end, .5);
47 return new MapViewPositionAndRotation(center, upsideTheta(best));
48 }).orElse(null);
49 }
50
51 private static double upsideTheta(HalfSegment best) {
52 double theta = theta(best.start, best.end);
53 if (theta < -Math.PI / 2) {
54 return theta + Math.PI;
55 } else if (theta > Math.PI / 2) {
56 return theta - Math.PI;
57 } else {
58 return theta;
59 }
60 }
61
62 @Override
63 public boolean supportsGlyphVector() {
64 return true;
65 }
66
67 @Override
68 public List<GlyphVector> generateGlyphVectors(MapViewPath path, Rectangle2D nb, List<GlyphVector> gvs,
69 boolean isDoubleTranslationBug) {
70 // Find the position on the way the font should be placed.
71 // If none is found, use the middle of the way.
72 double middleOffset = findOptimalWayPosition(nb, path).map(segment -> segment.offset)
73 .orElse(path.getLength() / 2);
74
75 // Check that segment of the way. Compute in which direction the text should be rendered.
76 // It is rendered in a way that ensures that at least 50% of the text are rotated with the right side up.
77 UpsideComputingVisitor upside = new UpsideComputingVisitor(middleOffset - nb.getWidth() / 2,
78 middleOffset + nb.getWidth() / 2);
79 path.visitLine(upside);
80 boolean doRotateText = upside.shouldRotateText();
81
82 // Compute the list of glyphs to draw, along with their offset on the current line.
83 List<OffsetGlyph> offsetGlyphs = computeOffsetGlyphs(gvs,
84 middleOffset + (doRotateText ? 1 : -1) * nb.getWidth() / 2, doRotateText);
85
86 // Order the glyphs along the line to ensure that they are drawn corretly.
87 Collections.sort(offsetGlyphs, Comparator.comparing(OffsetGlyph::getOffset));
88
89 // Now translate all glyphs. This will modify the glyphs stored in gvs.
90 path.visitLine(new GlyphRotatingVisitor(offsetGlyphs, isDoubleTranslationBug));
91 return gvs;
92 }
93
94 /**
95 * Create a list of glyphs with an offset along the way
96 * @param gvs The list of glyphs
97 * @param startOffset The offset in the line
98 * @param rotateText Rotate the text by 180°
99 * @return The list of glyphs.
100 */
101 private static List<OffsetGlyph> computeOffsetGlyphs(List<GlyphVector> gvs, double startOffset, boolean rotateText) {
102 double offset = startOffset;
103 ArrayList<OffsetGlyph> offsetGlyphs = new ArrayList<>();
104 for (GlyphVector gv : gvs) {
105 double gvOffset = offset;
106 IntStream.range(0, gv.getNumGlyphs())
107 .mapToObj(i -> new OffsetGlyph(gvOffset, rotateText, gv, i))
108 .forEach(offsetGlyphs::add);
109 offset += (rotateText ? -1 : 1) + gv.getLogicalBounds().getBounds2D().getWidth();
110 }
111 return offsetGlyphs;
112 }
113
114 private static Optional<HalfSegment> findOptimalWayPosition(Rectangle2D rect, MapViewPath path) {
115 // find half segments that are long enough to draw text on (don't draw text over the cross hair in the center of each segment)
116 List<HalfSegment> longHalfSegment = new ArrayList<>();
117 double minSegmentLength = 2 * (rect.getWidth() + 4);
118 double length = path.visitLine((inLineOffset, start, end, startIsOldEnd) -> {
119 double segmentLength = start.distanceToInView(end);
120 if (segmentLength > minSegmentLength) {
121 MapViewPoint center = start.interpolate(end, .5);
122 double q = computeQuality(start, center);
123 // prefer the first one for quality equality.
124 longHalfSegment.add(new HalfSegment(start, center, q + .1, inLineOffset + .25 * segmentLength));
125
126 q = computeQuality(center, end);
127 longHalfSegment.add(new HalfSegment(center, end, q, inLineOffset + .75 * segmentLength));
128 }
129 });
130
131 // find the segment with the best quality. If there are several with best quality, the one close to the center is prefered.
132 return longHalfSegment.stream().max(
133 Comparator.comparingDouble(segment -> segment.quality - 1e-5 * Math.abs(segment.offset - length / 2)));
134 }
135
136 private static double computeQuality(MapViewPoint p1, MapViewPoint p2) {
137 double q = 0;
138 if (p1.isInView()) {
139 q += 1;
140 }
141 if (p2.isInView()) {
142 q += 1;
143 }
144 return q;
145 }
146
147 /**
148 * A half segment that can be used to place text on it. Used in the drawTextOnPath algorithm.
149 * @author Michael Zangl
150 */
151 private static class HalfSegment {
152 /**
153 * start point of half segment
154 */
155 private final MapViewPoint start;
156
157 /**
158 * end point of half segment
159 */
160 private final MapViewPoint end;
161
162 /**
163 * quality factor (off screen / partly on screen / fully on screen)
164 */
165 private final double quality;
166
167 /**
168 * The offset in the path.
169 */
170 private final double offset;
171
172 /**
173 * Create a new half segment
174 * @param start The start along the way
175 * @param end The end of the segment
176 * @param quality A quality factor.
177 * @param offset The offset in the path.
178 */
179 HalfSegment(MapViewPoint start, MapViewPoint end, double quality, double offset) {
180 super();
181 this.start = start;
182 this.end = end;
183 this.quality = quality;
184 this.offset = offset;
185 }
186
187 @Override
188 public String toString() {
189 return "HalfSegment [start=" + start + ", end=" + end + ", quality=" + quality + ']';
190 }
191 }
192
193 /**
194 * A visitor that computes the side of the way that is the upper one for each segment and computes the dominant upper side of the way.
195 * This is used to always place at least 50% of the text correctly.
196 */
197 private static class UpsideComputingVisitor implements PathSegmentConsumer {
198
199 private final double startOffset;
200 private final double endOffset;
201
202 private double upsideUpLines;
203 private double upsideDownLines;
204
205 UpsideComputingVisitor(double startOffset, double endOffset) {
206 super();
207 this.startOffset = startOffset;
208 this.endOffset = endOffset;
209 }
210
211 @Override
212 public void addLineBetween(double inLineOffset, MapViewPoint start, MapViewPoint end, boolean startIsOldEnd) {
213 if (inLineOffset > endOffset) {
214 return;
215 }
216 double length = start.distanceToInView(end);
217 if (inLineOffset + length < startOffset) {
218 return;
219 }
220
221 double segmentStart = Math.max(inLineOffset, startOffset);
222 double segmentEnd = Math.min(inLineOffset + length, endOffset);
223
224 double segmentLength = segmentEnd - segmentStart;
225
226 if (start.getInViewX() < end.getInViewX()) {
227 upsideUpLines += segmentLength;
228 } else {
229 upsideDownLines += segmentLength;
230 }
231 }
232
233 /**
234 * Check if the text should be rotated by 180°
235 * @return if the text should be rotated.
236 */
237 boolean shouldRotateText() {
238 return upsideUpLines < upsideDownLines;
239 }
240 }
241
242 /**
243 * Rotate the glyphs along a path.
244 */
245 private class GlyphRotatingVisitor implements PathSegmentConsumer {
246 private final Iterator<OffsetGlyph> gvs;
247 private final boolean isDoubleTranslationBug;
248 private OffsetGlyph next;
249
250 /**
251 * Create a new {@link GlyphRotatingVisitor}
252 * @param gvs The glyphs to draw. Sorted along the line
253 * @param isDoubleTranslationBug true to fix a double translation bug.
254 */
255 GlyphRotatingVisitor(List<OffsetGlyph> gvs, boolean isDoubleTranslationBug) {
256 this.isDoubleTranslationBug = isDoubleTranslationBug;
257 this.gvs = gvs.iterator();
258 takeNext();
259 while (next != null && next.offset < 0) {
260 // skip them
261 takeNext();
262 }
263 }
264
265 private void takeNext() {
266 if (gvs.hasNext()) {
267 next = gvs.next();
268 } else {
269 next = null;
270 }
271 }
272
273 @Override
274 public void addLineBetween(double inLineOffset, MapViewPoint start, MapViewPoint end, boolean startIsOldEnd) {
275 double segLength = start.distanceToInView(end);
276 double segEnd = inLineOffset + segLength;
277 double theta = theta(start, end);
278 while (next != null && next.offset < segEnd) {
279 Rectangle2D rect = next.getBounds();
280 double centerY = 0;
281 MapViewPoint p = start.interpolate(end, (next.offset - inLineOffset) / segLength);
282
283 AffineTransform trfm = new AffineTransform();
284 trfm.translate(-rect.getCenterX(), -centerY);
285 trfm.translate(p.getInViewX(), p.getInViewY());
286 trfm.rotate(theta + next.preRotate, rect.getWidth() / 2, centerY);
287 trfm.translate(0, next.glyph.getFont().getSize2D() * .25);
288 trfm.translate(0, yOffset);
289 if (isDoubleTranslationBug) {
290 // scale the translation components by one half
291 AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(),
292 -0.5 * trfm.getTranslateY());
293 tmp.concatenate(trfm);
294 trfm = tmp;
295 }
296 next.glyph.setGlyphTransform(next.glyphIndex, trfm);
297 takeNext();
298 }
299 }
300 }
301
302 private static class OffsetGlyph {
303 private final double offset;
304 private final double preRotate;
305 private final GlyphVector glyph;
306 private final int glyphIndex;
307
308 OffsetGlyph(double offset, boolean rotateText, GlyphVector glyph, int glyphIndex) {
309 super();
310 this.preRotate = rotateText ? Math.PI : 0;
311 this.glyph = glyph;
312 this.glyphIndex = glyphIndex;
313 Rectangle2D rect = getBounds();
314 this.offset = offset + (rotateText ? -1 : 1) * (rect.getX() + rect.getWidth() / 2);
315 }
316
317 Rectangle2D getBounds() {
318 return glyph.getGlyphLogicalBounds(glyphIndex).getBounds2D();
319 }
320
321 double getOffset() {
322 return offset;
323 }
324
325 @Override
326 public String toString() {
327 return "OffsetGlyph [offset=" + offset + ", preRotate=" + preRotate + ", glyphIndex=" + glyphIndex + ']';
328 }
329 }
330
331 private static double theta(MapViewPoint start, MapViewPoint end) {
332 return Math.atan2(end.getInViewY() - start.getInViewY(), end.getInViewX() - start.getInViewX());
333 }
334}
Note: See TracBrowser for help on using the repository browser.