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

Last change on this file since 12476 was 12476, checked in by michael2402, 7 years ago

Fix #15006: Separate offset handling for ways, areas and node. Handle offset for all three of them.

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