Changeset 11748 in josm for trunk/src/org/openstreetmap/josm/data/osm/visitor/paint
- Timestamp:
- 2017-03-20T17:47:47+01:00 (7 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java
r11746 r11748 29 29 import java.util.Collection; 30 30 import java.util.Collections; 31 import java.util.Comparator;32 31 import java.util.HashMap; 33 32 import java.util.Iterator; … … 41 40 import java.util.function.Consumer; 42 41 import java.util.function.Supplier; 43 import java.util.stream.Collectors;44 42 45 43 import javax.swing.AbstractButton; … … 66 64 import org.openstreetmap.josm.gui.NavigatableComponent; 67 65 import org.openstreetmap.josm.gui.draw.MapViewPath; 66 import org.openstreetmap.josm.gui.draw.MapViewPositionAndRotation; 68 67 import org.openstreetmap.josm.gui.mappaint.ElemStyles; 69 68 import org.openstreetmap.josm.gui.mappaint.MapPaintStyles; … … 76 75 import org.openstreetmap.josm.gui.mappaint.styleelement.MapImage; 77 76 import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement; 78 import org.openstreetmap.josm.gui.mappaint.styleelement.PositionForAreaStrategy;79 77 import org.openstreetmap.josm.gui.mappaint.styleelement.RepeatImageElement.LineImageAlignment; 80 78 import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement; … … 82 80 import org.openstreetmap.josm.gui.mappaint.styleelement.TextElement; 83 81 import org.openstreetmap.josm.gui.mappaint.styleelement.TextLabel; 82 import org.openstreetmap.josm.gui.mappaint.styleelement.placement.PositionForAreaStrategy; 84 83 import org.openstreetmap.josm.tools.CompositeList; 85 84 import org.openstreetmap.josm.tools.Geometry; … … 340 339 341 340 /** 342 * Displays text at specified position including its halo, if applicable.343 *344 * @param gv Text's glyphs to display. If {@code null}, use text from {@code s} instead.345 * @param s text to display if {@code gv} is {@code null}346 * @param x X position347 * @param y Y position348 * @param disabled {@code true} if element is disabled (filtered out)349 * @param text text style to use350 */351 private void displayText(GlyphVector gv, String s, int x, int y, boolean disabled, TextLabel text) {352 if (gv == null && s.isEmpty()) return;353 if (isInactiveMode || disabled) {354 g.setColor(inactiveColor);355 if (gv != null) {356 g.drawGlyphVector(gv, x, y);357 } else {358 g.setFont(text.font);359 g.drawString(s, x, y);360 }361 } else if (text.haloRadius != null) {362 g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));363 g.setColor(text.haloColor);364 Shape textOutline;365 if (gv == null) {366 FontRenderContext frc = g.getFontRenderContext();367 TextLayout tl = new TextLayout(s, text.font, frc);368 textOutline = tl.getOutline(AffineTransform.getTranslateInstance(x, y));369 } else {370 textOutline = gv.getOutline(x, y);371 }372 g.draw(textOutline);373 g.setStroke(new BasicStroke());374 g.setColor(text.color);375 g.fill(textOutline);376 } else {377 g.setColor(text.color);378 if (gv != null) {379 g.drawGlyphVector(gv, x, y);380 } else {381 g.setFont(text.font);382 g.drawString(s, x, y);383 }384 }385 }386 387 /**388 341 * Worker function for drawing areas. 389 342 * … … 400 353 * polygons) 401 354 * @param disabled If this should be drawn with a special disabled style. 402 * @param text The text to write on the area.403 */ 404 protected void drawArea(OsmPrimitive osm, Path2D.Doublepath, Color color,355 * @param text Ignored. Use {@link #drawText(OsmPrimitive, TextLabel)} instead. 356 */ 357 protected void drawArea(OsmPrimitive osm, MapViewPath path, Color color, 405 358 MapImage fillImage, Float extent, Path2D.Double pfClip, boolean disabled, TextLabel text) { 406 359 if (!isOutlineOnly && color.getAlpha() != 0) { 407 Shape area = path .createTransformedShape(mapState.getAffineTransform());360 Shape area = path; 408 361 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); 409 362 if (fillImage == null) { … … 450 403 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing); 451 404 } 452 453 drawAreaText(osm, text, path);454 }455 456 private void drawAreaText(OsmPrimitive osm, TextLabel text, Path2D.Double path) {457 if (text != null && isShowNames() && isAreaVisible(path)) {458 // abort if we can't compose the label to be rendered459 if (text.labelCompositionStrategy == null) return;460 String name = text.labelCompositionStrategy.compose(osm);461 if (name == null || name.isEmpty()) return;462 463 Shape area = path.createTransformedShape(mapState.getAffineTransform());464 FontMetrics fontMetrics = g.getFontMetrics(orderFont); // if slow, use cache465 Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)466 467 Rectangle2D centeredNBounds = text.getLabelPositionStrategy().findLabelPlacement(area, nb);468 if (centeredNBounds != null) {469 Font defaultFont = g.getFont();470 int x = (int) (centeredNBounds.getMinX() - nb.getMinX());471 int y = (int) (centeredNBounds.getMinY() - nb.getMinY());472 displayText(null, name, x, y, osm.isDisabled(), text);473 g.setFont(defaultFont);474 } else if (Main.isTraceEnabled()) {475 Main.trace("Couldn't find a correct label placement for "+osm+" / "+name);476 }477 }478 405 } 479 406 … … 495 422 if (!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) { 496 423 for (PolyData pd : multipolygon.getCombinedPolygons()) { 497 Path2D.Double p = pd.get(); 424 MapViewPath p = new MapViewPath(mapState); 425 p.appendFromEastNorth(pd.get()); 426 p.setWindingRule(Path2D.WIND_EVEN_ODD); 498 427 Path2D.Double pfClip = null; 499 428 if (!isAreaVisible(p)) { … … 574 503 g.setFont(text.font); 575 504 576 int x = (int) (Math.round(p.getInViewX()) + text.xOffset); 577 int y = (int) (Math.round(p.getInViewY()) + text.yOffset); 505 FontRenderContext frc = g.getFontRenderContext(); 506 Rectangle2D bounds = text.font.getStringBounds(s, frc); 507 508 double x = Math.round(p.getInViewX()) + text.xOffset + bounds.getCenterX(); 509 double y = Math.round(p.getInViewY()) + text.yOffset + bounds.getCenterY(); 578 510 /** 579 511 * … … 591 523 x += box.x + box.width + 2; 592 524 } else { 593 FontRenderContext frc = g.getFontRenderContext();594 Rectangle2D bounds = text.font.getStringBounds(s, frc);595 525 int textWidth = (int) bounds.getWidth(); 596 526 if (bs.hAlign == HorizontalTextAlignment.CENTER) { … … 604 534 y += box.y + box.height; 605 535 } else { 606 FontRenderContext frc = g.getFontRenderContext();607 536 LineMetrics metrics = text.font.getLineMetrics(s, frc); 608 537 if (bs.vAlign == VerticalTextAlignment.ABOVE) { … … 616 545 } else throw new AssertionError(); 617 546 } 618 displayText(null, s, x, y, n.isDisabled(), text); 547 548 displayText(n, text, s, bounds, new MapViewPositionAndRotation(mapState.getForView(x, y), 0)); 619 549 g.setFont(defaultFont); 620 550 } … … 757 687 758 688 forEachPolygon(osm, path -> { 759 Shape area = path.createTransformedShape(mapState.getAffineTransform()); 760 Rectangle2D placement = iconPosition.findLabelPlacement(area, iconRect); 689 MapViewPositionAndRotation placement = iconPosition.findLabelPlacement(path, iconRect); 761 690 if (placement == null) { 762 691 return; 763 692 } 764 MapViewPoint p = mapState.getForView(placement.getCenterX(), placement.getCenterY());765 drawIcon(p, img, disabled, selected, member, theta , (g, r) -> {693 MapViewPoint p = placement.getPoint(); 694 drawIcon(p, img, disabled, selected, member, theta + placement.getRotation(), (g, r) -> { 766 695 if (useStrokes) { 767 696 g.setStroke(new BasicStroke(2)); … … 1071 1000 1072 1001 /** 1073 * A half segment that can be used to place text on it. Used in the drawTextOnPath algorithm.1074 * @author Michael Zangl1075 */1076 private static class HalfSegment {1077 /**1078 * start point of half segment (as length along the way)1079 */1080 final double start;1081 /**1082 * end point of half segment (as length along the way)1083 */1084 final double end;1085 /**1086 * quality factor (off screen / partly on screen / fully on screen)1087 */1088 final double quality;1089 1090 /**1091 * Create a new half segment1092 * @param start The start along the way1093 * @param end The end of the segment1094 * @param quality A quality factor.1095 */1096 HalfSegment(double start, double end, double quality) {1097 super();1098 this.start = start;1099 this.end = end;1100 this.quality = quality;1101 }1102 1103 @Override1104 public String toString() {1105 return "HalfSegment [start=" + start + ", end=" + end + ", quality=" + quality + "]";1106 }1107 }1108 1109 /**1110 1002 * Draws a text for the given primitive 1111 1003 * @param osm The primitive to draw the text for … … 1114 1006 */ 1115 1007 public void drawText(OsmPrimitive osm, TextLabel text) { 1116 PositionForAreaStrategy position = text.getLabelPositionStrategy(); 1117 if (position.supportsGlyphVector()) { 1118 if (osm instanceof Way) { 1119 // we might allow this for the outline of relations as well. 1120 drawTextOnPath((Way) osm, text); 1121 } 1008 if (!isShowNames()) { 1009 return; 1010 } 1011 String name = text.getString(osm); 1012 if (name == null || name.isEmpty()) { 1013 return; 1014 } 1015 1016 FontMetrics fontMetrics = g.getFontMetrics(text.font); // if slow, use cache 1017 Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font) 1018 1019 Font defaultFont = g.getFont(); 1020 forEachPolygon(osm, path -> { 1021 //TODO: Ignore areas that are out of bounds. 1022 PositionForAreaStrategy position = text.getLabelPositionStrategy(); 1023 MapViewPositionAndRotation center = text.getLabelPositionStrategy().findLabelPlacement(path, nb); 1024 if (center != null) { 1025 displayText(osm, text, name, nb, center); 1026 } else if (position.supportsGlyphVector()) { 1027 List<GlyphVector> gvs = Utils.getGlyphVectorsBidi(name, text.font, g.getFontRenderContext()); 1028 1029 List<GlyphVector> translatedGvs = position.generateGlyphVectors(path, nb, gvs, isGlyphVectorDoubleTranslationBug(text.font)); 1030 displayText(() -> translatedGvs.forEach(gv -> g.drawGlyphVector(gv, 0, 0)), 1031 () -> translatedGvs.stream().collect( 1032 () -> new Path2D.Double(), 1033 (p, gv) -> p.append(gv.getOutline(0, 0), false), 1034 (p1, p2) -> p1.append(p2, false)), 1035 osm.isDisabled(), text); 1036 } else if (Main.isTraceEnabled()) { 1037 Main.trace("Couldn't find a correct label placement for " + osm + " / " + name); 1038 } 1039 }); 1040 g.setFont(defaultFont); 1041 } 1042 1043 private void displayText(OsmPrimitive osm, TextLabel text, String name, Rectangle2D nb, 1044 MapViewPositionAndRotation center) { 1045 AffineTransform at = AffineTransform.getTranslateInstance(center.getPoint().getInViewX(), center.getPoint().getInViewY()); 1046 at.rotate(center.getRotation()); 1047 at.translate(-nb.getCenterX(), -nb.getCenterY()); 1048 displayText(() -> { 1049 AffineTransform defaultTransform = g.getTransform(); 1050 g.setTransform(at); 1051 g.setFont(text.font); 1052 g.drawString(name, 0, 0); 1053 g.setTransform(defaultTransform); 1054 }, () -> { 1055 FontRenderContext frc = g.getFontRenderContext(); 1056 TextLayout tl = new TextLayout(name, text.font, frc); 1057 return tl.getOutline(at); 1058 }, osm.isDisabled(), text); 1059 } 1060 1061 /** 1062 * Displays text at specified position including its halo, if applicable. 1063 * 1064 * @param fill The function that fills the text 1065 * @param outline The function to draw the outline 1066 * @param disabled {@code true} if element is disabled (filtered out) 1067 * @param text text style to use 1068 */ 1069 private void displayText(Runnable fill, Supplier<Shape> outline, boolean disabled, TextLabel text) { 1070 if (isInactiveMode || disabled) { 1071 g.setColor(inactiveColor); 1072 fill.run(); 1073 } else if (text.haloRadius != null) { 1074 g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND)); 1075 g.setColor(text.haloColor); 1076 Shape textOutline = outline.get(); 1077 g.draw(textOutline); 1078 g.setStroke(new BasicStroke()); 1079 g.setColor(text.color); 1080 g.fill(textOutline); 1122 1081 } else { 1123 forEachPolygon(osm, path -> drawAreaText(osm, text, path)); 1082 g.setColor(text.color); 1083 fill.run(); 1124 1084 } 1125 1085 } … … 1130 1090 * @param consumer The consumer to call. 1131 1091 */ 1132 private void forEachPolygon(OsmPrimitive osm, Consumer< Path2D.Double> consumer) {1092 private void forEachPolygon(OsmPrimitive osm, Consumer<MapViewPath> consumer) { 1133 1093 if (osm instanceof Way) { 1134 1094 consumer.accept(getPath((Way) osm)); … … 1137 1097 if (!multipolygon.getOuterWays().isEmpty()) { 1138 1098 for (PolyData pd : multipolygon.getCombinedPolygons()) { 1139 consumer.accept(pd.get()); 1099 MapViewPath path = new MapViewPath(mapState); 1100 path.appendFromEastNorth(pd.get()); 1101 consumer.accept(path); 1140 1102 } 1141 1103 } … … 1147 1109 * @param way The way to draw the text on. 1148 1110 * @param text The text definition (font/.../text content) to draw. 1149 */ 1111 * @deprecated Use {@link #drawText(OsmPrimitive, TextLabel)} instead. 1112 */ 1113 @Deprecated 1150 1114 public void drawTextOnPath(Way way, TextLabel text) { 1151 if (way == null || text == null) 1152 return; 1153 String name = text.getString(way); 1154 if (name == null || name.isEmpty()) 1155 return; 1156 1157 FontMetrics fontMetrics = g.getFontMetrics(text.font); 1158 Rectangle2D rec = fontMetrics.getStringBounds(name, g); 1159 1160 Rectangle bounds = g.getClipBounds(); 1161 1162 List<MapViewPoint> points = way.getNodes().stream().map(mapState::getPointFor).collect(Collectors.toList()); 1163 1164 // 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) 1165 List<HalfSegment> longHalfSegment = new ArrayList<>(); 1166 1167 double pathLength = computePath(2 * (rec.getWidth() + 4), bounds, points, longHalfSegment); 1168 1169 if (rec.getWidth() > pathLength) 1170 return; 1171 1172 double t1, t2; 1173 1174 if (!longHalfSegment.isEmpty()) { 1175 // find the segment with the best quality. If there are several with best quality, the one close to the center is prefered. 1176 Optional<HalfSegment> besto = longHalfSegment.stream().max( 1177 Comparator.comparingDouble(segment -> 1178 segment.quality - 1e-5 * Math.abs(0.5 * (segment.end + segment.start) - 0.5 * pathLength) 1179 )); 1180 if (!besto.isPresent()) 1181 throw new IllegalStateException("Unable to find the segment with the best quality for " + way); 1182 HalfSegment best = besto.get(); 1183 double remaining = best.end - best.start - rec.getWidth(); // total space left and right from the text 1184 // The space left and right of the text should be distributed 20% - 80% (towards the center), 1185 // but the smaller space should not be less than 7 px. 1186 // However, if the total remaining space is less than 14 px, then distribute it evenly. 1187 double smallerSpace = Math.min(Math.max(0.2 * remaining, 7), 0.5 * remaining); 1188 if ((best.end + best.start)/2 < pathLength/2) { 1189 t2 = best.end - smallerSpace; 1190 t1 = t2 - rec.getWidth(); 1191 } else { 1192 t1 = best.start + smallerSpace; 1193 t2 = t1 + rec.getWidth(); 1194 } 1195 } else { 1196 // doesn't fit into one half-segment -> just put it in the center of the way 1197 t1 = pathLength/2 - rec.getWidth()/2; 1198 t2 = pathLength/2 + rec.getWidth()/2; 1199 } 1200 t1 /= pathLength; 1201 t2 /= pathLength; 1202 1203 double[] p1 = pointAt(t1, points, pathLength); 1204 double[] p2 = pointAt(t2, points, pathLength); 1205 1206 if (p1 == null || p2 == null) 1207 return; 1208 1209 double angleOffset; 1210 double offsetSign; 1211 double tStart; 1212 1213 if (p1[0] < p2[0] && 1214 p1[2] < Math.PI/2 && 1215 p1[2] > -Math.PI/2) { 1216 angleOffset = 0; 1217 offsetSign = 1; 1218 tStart = t1; 1219 } else { 1220 angleOffset = Math.PI; 1221 offsetSign = -1; 1222 tStart = t2; 1223 } 1224 1225 List<GlyphVector> gvs = Utils.getGlyphVectorsBidi(name, text.font, g.getFontRenderContext()); 1226 double gvOffset = 0; 1227 for (GlyphVector gv : gvs) { 1228 double gvWidth = gv.getLogicalBounds().getBounds2D().getWidth(); 1229 for (int i = 0; i < gv.getNumGlyphs(); ++i) { 1230 Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D(); 1231 double t = tStart + offsetSign * (gvOffset + rect.getX() + rect.getWidth()/2) / pathLength; 1232 double[] p = pointAt(t, points, pathLength); 1233 if (p != null) { 1234 AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]); 1235 trfm.rotate(p[2]+angleOffset); 1236 double off = -rect.getY() - rect.getHeight()/2 + text.yOffset; 1237 trfm.translate(-rect.getWidth()/2, off); 1238 if (isGlyphVectorDoubleTranslationBug(text.font)) { 1239 // scale the translation components by one half 1240 AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY()); 1241 tmp.concatenate(trfm); 1242 trfm = tmp; 1243 } 1244 gv.setGlyphTransform(i, trfm); 1245 } 1246 } 1247 displayText(gv, null, 0, 0, way.isDisabled(), text); 1248 gvOffset += gvWidth; 1249 } 1250 } 1251 1252 private static double computePath(double minSegmentLength, Rectangle bounds, List<MapViewPoint> points, 1253 List<HalfSegment> longHalfSegment) { 1254 MapViewPoint lastPoint = points.get(0); 1255 double pathLength = 0; 1256 for (MapViewPoint p : points.subList(1, points.size())) { 1257 double segmentLength = p.distanceToInView(lastPoint); 1258 if (segmentLength > minSegmentLength) { 1259 Point2D center = new Point2D.Double((lastPoint.getInViewX() + p.getInViewX())/2, (lastPoint.getInViewY() + p.getInViewY())/2); 1260 double q = computeQuality(bounds, lastPoint, center); 1261 // prefer the first one for quality equality. 1262 longHalfSegment.add(new HalfSegment(pathLength, pathLength + segmentLength / 2, q)); 1263 1264 q = 0; 1265 if (bounds != null) { 1266 if (bounds.contains(center) && bounds.contains(p.getInView())) { 1267 q = 2; 1268 } else if (bounds.contains(center) || bounds.contains(p.getInView())) { 1269 q = 1; 1270 } 1271 } 1272 longHalfSegment.add(new HalfSegment(pathLength + segmentLength / 2, pathLength + segmentLength, q)); 1273 } 1274 pathLength += segmentLength; 1275 lastPoint = p; 1276 } 1277 return pathLength; 1278 } 1279 1280 private static double computeQuality(Rectangle bounds, MapViewPoint p1, Point2D p2) { 1281 double q = 0; 1282 if (bounds != null) { 1283 if (bounds.contains(p1.getInView())) { 1284 q += 1; 1285 } 1286 if (bounds.contains(p2)) { 1287 q += 1; 1288 } 1289 } 1290 return q; 1115 // NOP. 1291 1116 } 1292 1117 … … 1484 1309 } 1485 1310 1486 private static Path2D.Double getPath(Way w) { 1487 Path2D.Double path = new Path2D.Double(); 1488 boolean initial = true; 1489 for (Node n : w.getNodes()) { 1490 EastNorth p = n.getEastNorth(); 1491 if (p != null) { 1492 if (initial) { 1493 path.moveTo(p.getX(), p.getY()); 1494 initial = false; 1495 } else { 1496 path.lineTo(p.getX(), p.getY()); 1497 } 1498 } 1499 } 1311 private MapViewPath getPath(Way w) { 1312 MapViewPath path = new MapViewPath(mapState); 1500 1313 if (w.isClosed()) { 1501 path.closePath(); 1314 path.appendClosed(w.getNodes(), false); 1315 } else { 1316 path.append(w.getNodes(), false); 1502 1317 } 1503 1318 return path; … … 1631 1446 public boolean isShowNames() { 1632 1447 return showNames; 1633 }1634 1635 private static double[] pointAt(double t, List<MapViewPoint> poly, double pathLength) {1636 double totalLen = t * pathLength;1637 double curLen = 0;1638 double dx, dy;1639 double segLen;1640 1641 // Yes, it is inefficient to iterate from the beginning for each glyph.1642 // Can be optimized if it turns out to be slow.1643 for (int i = 1; i < poly.size(); ++i) {1644 dx = poly.get(i).getInViewX() - poly.get(i - 1).getInViewX();1645 dy = poly.get(i).getInViewY() - poly.get(i - 1).getInViewY();1646 segLen = Math.sqrt(dx*dx + dy*dy);1647 if (totalLen > curLen + segLen) {1648 curLen += segLen;1649 continue;1650 }1651 return new double[] {1652 poly.get(i - 1).getInViewX() + (totalLen - curLen) / segLen * dx,1653 poly.get(i - 1).getInViewY() + (totalLen - curLen) / segLen * dy,1654 Math.atan2(dy, dx)};1655 }1656 return null;1657 1448 } 1658 1449
Note:
See TracChangeset
for help on using the changeset viewer.