| 1 | // License: GPL. For details, see LICENSE file.
 | 
|---|
| 2 | package org.openstreetmap.josm.data.osm.visitor.paint;
 | 
|---|
| 3 | 
 | 
|---|
| 4 | import java.awt.AlphaComposite;
 | 
|---|
| 5 | import java.awt.BasicStroke;
 | 
|---|
| 6 | import java.awt.Color;
 | 
|---|
| 7 | import java.awt.Component;
 | 
|---|
| 8 | import java.awt.Dimension;
 | 
|---|
| 9 | import java.awt.Font;
 | 
|---|
| 10 | import java.awt.FontMetrics;
 | 
|---|
| 11 | import java.awt.Graphics2D;
 | 
|---|
| 12 | import java.awt.Image;
 | 
|---|
| 13 | import java.awt.Point;
 | 
|---|
| 14 | import java.awt.Rectangle;
 | 
|---|
| 15 | import java.awt.RenderingHints;
 | 
|---|
| 16 | import java.awt.Shape;
 | 
|---|
| 17 | import java.awt.TexturePaint;
 | 
|---|
| 18 | import java.awt.font.FontRenderContext;
 | 
|---|
| 19 | import java.awt.font.GlyphVector;
 | 
|---|
| 20 | import java.awt.font.LineMetrics;
 | 
|---|
| 21 | import java.awt.font.TextLayout;
 | 
|---|
| 22 | import java.awt.geom.AffineTransform;
 | 
|---|
| 23 | import java.awt.geom.Path2D;
 | 
|---|
| 24 | import java.awt.geom.Point2D;
 | 
|---|
| 25 | import java.awt.geom.Rectangle2D;
 | 
|---|
| 26 | import java.awt.geom.RoundRectangle2D;
 | 
|---|
| 27 | import java.awt.image.BufferedImage;
 | 
|---|
| 28 | import java.util.ArrayList;
 | 
|---|
| 29 | import java.util.Arrays;
 | 
|---|
| 30 | import java.util.Collection;
 | 
|---|
| 31 | import java.util.HashMap;
 | 
|---|
| 32 | import java.util.Iterator;
 | 
|---|
| 33 | import java.util.List;
 | 
|---|
| 34 | import java.util.Map;
 | 
|---|
| 35 | import java.util.Optional;
 | 
|---|
| 36 | import java.util.concurrent.ForkJoinPool;
 | 
|---|
| 37 | import java.util.concurrent.TimeUnit;
 | 
|---|
| 38 | import java.util.function.BiConsumer;
 | 
|---|
| 39 | import java.util.function.Consumer;
 | 
|---|
| 40 | import java.util.function.Supplier;
 | 
|---|
| 41 | 
 | 
|---|
| 42 | import javax.swing.AbstractButton;
 | 
|---|
| 43 | import javax.swing.FocusManager;
 | 
|---|
| 44 | 
 | 
|---|
| 45 | import org.openstreetmap.josm.Main;
 | 
|---|
| 46 | import org.openstreetmap.josm.data.Bounds;
 | 
|---|
| 47 | import org.openstreetmap.josm.data.coor.EastNorth;
 | 
|---|
| 48 | import org.openstreetmap.josm.data.osm.BBox;
 | 
|---|
| 49 | import org.openstreetmap.josm.data.osm.DataSet;
 | 
|---|
| 50 | import org.openstreetmap.josm.data.osm.Node;
 | 
|---|
| 51 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
 | 
|---|
| 52 | import org.openstreetmap.josm.data.osm.OsmUtils;
 | 
|---|
| 53 | import org.openstreetmap.josm.data.osm.Relation;
 | 
|---|
| 54 | import org.openstreetmap.josm.data.osm.RelationMember;
 | 
|---|
| 55 | import org.openstreetmap.josm.data.osm.Way;
 | 
|---|
| 56 | import org.openstreetmap.josm.data.osm.WaySegment;
 | 
|---|
| 57 | import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
 | 
|---|
| 58 | import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
 | 
|---|
| 59 | import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
 | 
|---|
| 60 | import org.openstreetmap.josm.data.preferences.AbstractProperty;
 | 
|---|
| 61 | import org.openstreetmap.josm.data.preferences.BooleanProperty;
 | 
|---|
| 62 | import org.openstreetmap.josm.data.preferences.IntegerProperty;
 | 
|---|
| 63 | import org.openstreetmap.josm.data.preferences.StringProperty;
 | 
|---|
| 64 | import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
 | 
|---|
| 65 | import org.openstreetmap.josm.gui.NavigatableComponent;
 | 
|---|
| 66 | import org.openstreetmap.josm.gui.draw.MapViewPath;
 | 
|---|
| 67 | import org.openstreetmap.josm.gui.draw.MapViewPositionAndRotation;
 | 
|---|
| 68 | import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement;
 | 
|---|
| 69 | import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.HorizontalTextAlignment;
 | 
|---|
| 70 | import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.VerticalTextAlignment;
 | 
|---|
| 71 | import org.openstreetmap.josm.gui.mappaint.styleelement.MapImage;
 | 
|---|
| 72 | import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement;
 | 
|---|
| 73 | import org.openstreetmap.josm.gui.mappaint.styleelement.RepeatImageElement.LineImageAlignment;
 | 
|---|
| 74 | import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
 | 
|---|
| 75 | import org.openstreetmap.josm.gui.mappaint.styleelement.Symbol;
 | 
|---|
| 76 | import org.openstreetmap.josm.gui.mappaint.styleelement.TextLabel;
 | 
|---|
| 77 | import org.openstreetmap.josm.gui.mappaint.styleelement.placement.PositionForAreaStrategy;
 | 
|---|
| 78 | import org.openstreetmap.josm.tools.CompositeList;
 | 
|---|
| 79 | import org.openstreetmap.josm.tools.Geometry;
 | 
|---|
| 80 | import org.openstreetmap.josm.tools.Geometry.AreaAndPerimeter;
 | 
|---|
| 81 | import org.openstreetmap.josm.tools.ImageProvider;
 | 
|---|
| 82 | import org.openstreetmap.josm.tools.JosmRuntimeException;
 | 
|---|
| 83 | import org.openstreetmap.josm.tools.Logging;
 | 
|---|
| 84 | import org.openstreetmap.josm.tools.Utils;
 | 
|---|
| 85 | import org.openstreetmap.josm.tools.bugreport.BugReport;
 | 
|---|
| 86 | 
 | 
|---|
| 87 | /**
 | 
|---|
| 88 |  * A map renderer which renders a map according to style rules in a set of style sheets.
 | 
|---|
| 89 |  * @since 486
 | 
|---|
| 90 |  */
 | 
|---|
| 91 | public class StyledMapRenderer extends AbstractMapRenderer {
 | 
|---|
| 92 | 
 | 
|---|
| 93 |     private static final ForkJoinPool THREAD_POOL =
 | 
|---|
| 94 |             Utils.newForkJoinPool("mappaint.StyledMapRenderer.style_creation.numberOfThreads", "styled-map-renderer-%d", Thread.NORM_PRIORITY);
 | 
|---|
| 95 | 
 | 
|---|
| 96 |     /**
 | 
|---|
| 97 |      * This stores a style and a primitive that should be painted with that style.
 | 
|---|
| 98 |      */
 | 
|---|
| 99 |     public static class StyleRecord implements Comparable<StyleRecord> {
 | 
|---|
| 100 |         private final StyleElement style;
 | 
|---|
| 101 |         private final OsmPrimitive osm;
 | 
|---|
| 102 |         private final int flags;
 | 
|---|
| 103 |         private final long order;
 | 
|---|
| 104 | 
 | 
|---|
| 105 |         StyleRecord(StyleElement style, OsmPrimitive osm, int flags) {
 | 
|---|
| 106 |             this.style = style;
 | 
|---|
| 107 |             this.osm = osm;
 | 
|---|
| 108 |             this.flags = flags;
 | 
|---|
| 109 | 
 | 
|---|
| 110 |             long order = 0;
 | 
|---|
| 111 |             if ((this.flags & FLAG_DISABLED) == 0) {
 | 
|---|
| 112 |                 order |= 1;
 | 
|---|
| 113 |             }
 | 
|---|
| 114 | 
 | 
|---|
| 115 |             order <<= 24;
 | 
|---|
| 116 |             order |= floatToFixed(this.style.majorZIndex, 24);
 | 
|---|
| 117 | 
 | 
|---|
| 118 |             // selected on top of member of selected on top of unselected
 | 
|---|
| 119 |             // FLAG_DISABLED bit is the same at this point, but we simply ignore it
 | 
|---|
| 120 |             order <<= 4;
 | 
|---|
| 121 |             order |= this.flags & 0xf;
 | 
|---|
| 122 | 
 | 
|---|
| 123 |             order <<= 24;
 | 
|---|
| 124 |             order |= floatToFixed(this.style.zIndex, 24);
 | 
|---|
| 125 | 
 | 
|---|
| 126 |             order <<= 1;
 | 
|---|
| 127 |             // simple node on top of icons and shapes
 | 
|---|
| 128 |             if (NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(this.style)) {
 | 
|---|
| 129 |                 order |= 1;
 | 
|---|
| 130 |             }
 | 
|---|
| 131 | 
 | 
|---|
| 132 |             this.order = order;
 | 
|---|
| 133 |         }
 | 
|---|
| 134 | 
 | 
|---|
| 135 |         /**
 | 
|---|
| 136 |          * Converts a float to a fixed point decimal so that the order stays the same.
 | 
|---|
| 137 |          *
 | 
|---|
| 138 |          * @param number The float to convert
 | 
|---|
| 139 |          * @param totalBits
 | 
|---|
| 140 |          *            Total number of bits. 1 sign bit. There should be at least 15 bits.
 | 
|---|
| 141 |          * @return The float converted to an integer.
 | 
|---|
| 142 |          */
 | 
|---|
| 143 |         protected static long floatToFixed(float number, int totalBits) {
 | 
|---|
| 144 |             long value = Float.floatToIntBits(number) & 0xffffffffL;
 | 
|---|
| 145 | 
 | 
|---|
| 146 |             boolean negative = (value & 0x80000000L) != 0;
 | 
|---|
| 147 |             // Invert the sign bit, so that negative numbers are lower
 | 
|---|
| 148 |             value ^= 0x80000000L;
 | 
|---|
| 149 |             // Now do the shift. Do it before accounting for negative numbers (symetry)
 | 
|---|
| 150 |             if (totalBits < 32) {
 | 
|---|
| 151 |                 value >>= (32 - totalBits);
 | 
|---|
| 152 |             }
 | 
|---|
| 153 |             // positive numbers are sorted now. Negative ones the wrong way.
 | 
|---|
| 154 |             if (negative) {
 | 
|---|
| 155 |                 // Negative number: re-map it
 | 
|---|
| 156 |                 value = (1L << (totalBits - 1)) - value;
 | 
|---|
| 157 |             }
 | 
|---|
| 158 |             return value;
 | 
|---|
| 159 |         }
 | 
|---|
| 160 | 
 | 
|---|
| 161 |         @Override
 | 
|---|
| 162 |         public int compareTo(StyleRecord other) {
 | 
|---|
| 163 |             int d = Long.compare(order, other.order);
 | 
|---|
| 164 |             if (d != 0) {
 | 
|---|
| 165 |                 return d;
 | 
|---|
| 166 |             }
 | 
|---|
| 167 | 
 | 
|---|
| 168 |             // newer primitives to the front
 | 
|---|
| 169 |             long id = this.osm.getUniqueId() - other.osm.getUniqueId();
 | 
|---|
| 170 |             if (id > 0)
 | 
|---|
| 171 |                 return 1;
 | 
|---|
| 172 |             if (id < 0)
 | 
|---|
| 173 |                 return -1;
 | 
|---|
| 174 | 
 | 
|---|
| 175 |             return Float.compare(this.style.objectZIndex, other.style.objectZIndex);
 | 
|---|
| 176 |         }
 | 
|---|
| 177 | 
 | 
|---|
| 178 |         /**
 | 
|---|
| 179 |          * Get the style for this style element.
 | 
|---|
| 180 |          * @return The style
 | 
|---|
| 181 |          */
 | 
|---|
| 182 |         public StyleElement getStyle() {
 | 
|---|
| 183 |             return style;
 | 
|---|
| 184 |         }
 | 
|---|
| 185 | 
 | 
|---|
| 186 |         /**
 | 
|---|
| 187 |          * Paints the primitive with the style.
 | 
|---|
| 188 |          * @param paintSettings The settings to use.
 | 
|---|
| 189 |          * @param painter The painter to paint the style.
 | 
|---|
| 190 |          */
 | 
|---|
| 191 |         public void paintPrimitive(MapPaintSettings paintSettings, StyledMapRenderer painter) {
 | 
|---|
| 192 |             style.paintPrimitive(
 | 
|---|
| 193 |                     osm,
 | 
|---|
| 194 |                     paintSettings,
 | 
|---|
| 195 |                     painter,
 | 
|---|
| 196 |                     (flags & FLAG_SELECTED) != 0,
 | 
|---|
| 197 |                     (flags & FLAG_OUTERMEMBER_OF_SELECTED) != 0,
 | 
|---|
| 198 |                     (flags & FLAG_MEMBER_OF_SELECTED) != 0
 | 
|---|
| 199 |             );
 | 
|---|
| 200 |         }
 | 
|---|
| 201 | 
 | 
|---|
| 202 |         @Override
 | 
|---|
| 203 |         public String toString() {
 | 
|---|
| 204 |             return "StyleRecord [style=" + style + ", osm=" + osm + ", flags=" + flags + "]";
 | 
|---|
| 205 |         }
 | 
|---|
| 206 |     }
 | 
|---|
| 207 | 
 | 
|---|
| 208 |     private static final Map<Font, Boolean> IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG = new HashMap<>();
 | 
|---|
| 209 | 
 | 
|---|
| 210 |     /**
 | 
|---|
| 211 |      * Check, if this System has the GlyphVector double translation bug.
 | 
|---|
| 212 |      *
 | 
|---|
| 213 |      * With this bug, <code>gv.setGlyphTransform(i, trfm)</code> has a different
 | 
|---|
| 214 |      * effect than on most other systems, namely the translation components
 | 
|---|
| 215 |      * ("m02" & "m12", {@link AffineTransform}) appear to be twice as large, as
 | 
|---|
| 216 |      * they actually are. The rotation is unaffected (scale & shear not tested
 | 
|---|
| 217 |      * so far).
 | 
|---|
| 218 |      *
 | 
|---|
| 219 |      * This bug has only been observed on Mac OS X, see #7841.
 | 
|---|
| 220 |      *
 | 
|---|
| 221 |      * After switch to Java 7, this test is a false positive on Mac OS X (see #10446),
 | 
|---|
| 222 |      * i.e. it returns true, but the real rendering code does not require any special
 | 
|---|
| 223 |      * handling.
 | 
|---|
| 224 |      * It hasn't been further investigated why the test reports a wrong result in
 | 
|---|
| 225 |      * this case, but the method has been changed to simply return false by default.
 | 
|---|
| 226 |      * (This can be changed with a setting in the advanced preferences.)
 | 
|---|
| 227 |      *
 | 
|---|
| 228 |      * @param font The font to check.
 | 
|---|
| 229 |      * @return false by default, but depends on the value of the advanced
 | 
|---|
| 230 |      * preference glyph-bug=false|true|auto, where auto is the automatic detection
 | 
|---|
| 231 |      * method which apparently no longer gives a useful result for Java 7.
 | 
|---|
| 232 |      */
 | 
|---|
| 233 |     public static boolean isGlyphVectorDoubleTranslationBug(Font font) {
 | 
|---|
| 234 |         Boolean cached = IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.get(font);
 | 
|---|
| 235 |         if (cached != null)
 | 
|---|
| 236 |             return cached;
 | 
|---|
| 237 |         String overridePref = Main.pref.get("glyph-bug", "auto");
 | 
|---|
| 238 |         if ("auto".equals(overridePref)) {
 | 
|---|
| 239 |             FontRenderContext frc = new FontRenderContext(null, false, false);
 | 
|---|
| 240 |             GlyphVector gv = font.createGlyphVector(frc, "x");
 | 
|---|
| 241 |             gv.setGlyphTransform(0, AffineTransform.getTranslateInstance(1000, 1000));
 | 
|---|
| 242 |             Shape shape = gv.getGlyphOutline(0);
 | 
|---|
| 243 |             if (Main.isTraceEnabled()) {
 | 
|---|
| 244 |                 Main.trace("#10446: shape: "+shape.getBounds());
 | 
|---|
| 245 |             }
 | 
|---|
| 246 |             // x is about 1000 on normal stystems and about 2000 when the bug occurs
 | 
|---|
| 247 |             int x = shape.getBounds().x;
 | 
|---|
| 248 |             boolean isBug = x > 1500;
 | 
|---|
| 249 |             IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.put(font, isBug);
 | 
|---|
| 250 |             return isBug;
 | 
|---|
| 251 |         } else {
 | 
|---|
| 252 |             boolean override = Boolean.parseBoolean(overridePref);
 | 
|---|
| 253 |             IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.put(font, override);
 | 
|---|
| 254 |             return override;
 | 
|---|
| 255 |         }
 | 
|---|
| 256 |     }
 | 
|---|
| 257 | 
 | 
|---|
| 258 |     private double circum;
 | 
|---|
| 259 |     private double scale;
 | 
|---|
| 260 | 
 | 
|---|
| 261 |     private MapPaintSettings paintSettings;
 | 
|---|
| 262 | 
 | 
|---|
| 263 |     private Color highlightColorTransparent;
 | 
|---|
| 264 | 
 | 
|---|
| 265 |     /**
 | 
|---|
| 266 |      * Flags used to store the primitive state along with the style. This is the normal style.
 | 
|---|
| 267 |      * <p>
 | 
|---|
| 268 |      * Not used in any public interfaces.
 | 
|---|
| 269 |      */
 | 
|---|
| 270 |     static final int FLAG_NORMAL = 0;
 | 
|---|
| 271 |     /**
 | 
|---|
| 272 |      * A primitive with {@link OsmPrimitive#isDisabled()}
 | 
|---|
| 273 |      */
 | 
|---|
| 274 |     static final int FLAG_DISABLED = 1;
 | 
|---|
| 275 |     /**
 | 
|---|
| 276 |      * A primitive with {@link OsmPrimitive#isMemberOfSelected()}
 | 
|---|
| 277 |      */
 | 
|---|
| 278 |     static final int FLAG_MEMBER_OF_SELECTED = 2;
 | 
|---|
| 279 |     /**
 | 
|---|
| 280 |      * A primitive with {@link OsmPrimitive#isSelected()}
 | 
|---|
| 281 |      */
 | 
|---|
| 282 |     static final int FLAG_SELECTED = 4;
 | 
|---|
| 283 |     /**
 | 
|---|
| 284 |      * A primitive with {@link OsmPrimitive#isOuterMemberOfSelected()}
 | 
|---|
| 285 |      */
 | 
|---|
| 286 |     static final int FLAG_OUTERMEMBER_OF_SELECTED = 8;
 | 
|---|
| 287 | 
 | 
|---|
| 288 |     private static final double PHI = Utils.toRadians(20);
 | 
|---|
| 289 |     private static final double cosPHI = Math.cos(PHI);
 | 
|---|
| 290 |     private static final double sinPHI = Math.sin(PHI);
 | 
|---|
| 291 |     /**
 | 
|---|
| 292 |      * If we should use left hand traffic.
 | 
|---|
| 293 |      */
 | 
|---|
| 294 |     private static final AbstractProperty<Boolean> PREFERENCE_LEFT_HAND_TRAFFIC
 | 
|---|
| 295 |             = new BooleanProperty("mappaint.lefthandtraffic", false).cached();
 | 
|---|
| 296 |     /**
 | 
|---|
| 297 |      * Indicates that the renderer should enable anti-aliasing
 | 
|---|
| 298 |      * @since 11758
 | 
|---|
| 299 |      */
 | 
|---|
| 300 |     public static final AbstractProperty<Boolean> PREFERENCE_ANTIALIASING_USE
 | 
|---|
| 301 |             = new BooleanProperty("mappaint.use-antialiasing", true).cached();
 | 
|---|
| 302 |     /**
 | 
|---|
| 303 |      * The mode that is used for anti-aliasing
 | 
|---|
| 304 |      * @since 11758
 | 
|---|
| 305 |      */
 | 
|---|
| 306 |     public static final AbstractProperty<String> PREFERENCE_TEXT_ANTIALIASING
 | 
|---|
| 307 |             = new StringProperty("mappaint.text-antialiasing", "default").cached();
 | 
|---|
| 308 | 
 | 
|---|
| 309 |     /**
 | 
|---|
| 310 |      * The line with to use for highlighting
 | 
|---|
| 311 |      */
 | 
|---|
| 312 |     private static final AbstractProperty<Integer> HIGHLIGHT_LINE_WIDTH = new IntegerProperty("mappaint.highlight.width", 4).cached();
 | 
|---|
| 313 |     private static final AbstractProperty<Integer> HIGHLIGHT_POINT_RADIUS = new IntegerProperty("mappaint.highlight.radius", 7).cached();
 | 
|---|
| 314 |     private static final AbstractProperty<Integer> WIDER_HIGHLIGHT = new IntegerProperty("mappaint.highlight.bigger-increment", 5).cached();
 | 
|---|
| 315 |     private static final AbstractProperty<Integer> HIGHLIGHT_STEP = new IntegerProperty("mappaint.highlight.step", 4).cached();
 | 
|---|
| 316 | 
 | 
|---|
| 317 |     private Collection<WaySegment> highlightWaySegments;
 | 
|---|
| 318 | 
 | 
|---|
| 319 |     //flag that activate wider highlight mode
 | 
|---|
| 320 |     private boolean useWiderHighlight;
 | 
|---|
| 321 | 
 | 
|---|
| 322 |     private boolean useStrokes;
 | 
|---|
| 323 |     private boolean showNames;
 | 
|---|
| 324 |     private boolean showIcons;
 | 
|---|
| 325 |     private boolean isOutlineOnly;
 | 
|---|
| 326 | 
 | 
|---|
| 327 |     private boolean leftHandTraffic;
 | 
|---|
| 328 |     private Object antialiasing;
 | 
|---|
| 329 | 
 | 
|---|
| 330 |     private Supplier<RenderBenchmarkCollector> benchmarkFactory = RenderBenchmarkCollector.defaultBenchmarkSupplier();
 | 
|---|
| 331 | 
 | 
|---|
| 332 |     /**
 | 
|---|
| 333 |      * Constructs a new {@code StyledMapRenderer}.
 | 
|---|
| 334 |      *
 | 
|---|
| 335 |      * @param g the graphics context. Must not be null.
 | 
|---|
| 336 |      * @param nc the map viewport. Must not be null.
 | 
|---|
| 337 |      * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
 | 
|---|
| 338 |      * look inactive. Example: rendering of data in an inactive layer using light gray as color only.
 | 
|---|
| 339 |      * @throws IllegalArgumentException if {@code g} is null
 | 
|---|
| 340 |      * @throws IllegalArgumentException if {@code nc} is null
 | 
|---|
| 341 |      */
 | 
|---|
| 342 |     public StyledMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
 | 
|---|
| 343 |         super(g, nc, isInactiveMode);
 | 
|---|
| 344 |         Component focusOwner = FocusManager.getCurrentManager().getFocusOwner();
 | 
|---|
| 345 |         useWiderHighlight = !(focusOwner instanceof AbstractButton || focusOwner == nc);
 | 
|---|
| 346 |     }
 | 
|---|
| 347 | 
 | 
|---|
| 348 |     private void displaySegments(MapViewPath path, Path2D orientationArrows, Path2D onewayArrows, Path2D onewayArrowsCasing,
 | 
|---|
| 349 |             Color color, BasicStroke line, BasicStroke dashes, Color dashedColor) {
 | 
|---|
| 350 |         g.setColor(isInactiveMode ? inactiveColor : color);
 | 
|---|
| 351 |         if (useStrokes) {
 | 
|---|
| 352 |             g.setStroke(line);
 | 
|---|
| 353 |         }
 | 
|---|
| 354 |         g.draw(path.computeClippedLine(g.getStroke()));
 | 
|---|
| 355 | 
 | 
|---|
| 356 |         if (!isInactiveMode && useStrokes && dashes != null) {
 | 
|---|
| 357 |             g.setColor(dashedColor);
 | 
|---|
| 358 |             g.setStroke(dashes);
 | 
|---|
| 359 |             g.draw(path.computeClippedLine(dashes));
 | 
|---|
| 360 |         }
 | 
|---|
| 361 | 
 | 
|---|
| 362 |         if (orientationArrows != null) {
 | 
|---|
| 363 |             g.setColor(isInactiveMode ? inactiveColor : color);
 | 
|---|
| 364 |             g.setStroke(new BasicStroke(line.getLineWidth(), line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
 | 
|---|
| 365 |             g.draw(orientationArrows);
 | 
|---|
| 366 |         }
 | 
|---|
| 367 | 
 | 
|---|
| 368 |         if (onewayArrows != null) {
 | 
|---|
| 369 |             g.setStroke(new BasicStroke(1, line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
 | 
|---|
| 370 |             g.fill(onewayArrowsCasing);
 | 
|---|
| 371 |             g.setColor(isInactiveMode ? inactiveColor : backgroundColor);
 | 
|---|
| 372 |             g.fill(onewayArrows);
 | 
|---|
| 373 |         }
 | 
|---|
| 374 | 
 | 
|---|
| 375 |         if (useStrokes) {
 | 
|---|
| 376 |             g.setStroke(new BasicStroke());
 | 
|---|
| 377 |         }
 | 
|---|
| 378 |     }
 | 
|---|
| 379 | 
 | 
|---|
| 380 |     /**
 | 
|---|
| 381 |      * Worker function for drawing areas.
 | 
|---|
| 382 |      *
 | 
|---|
| 383 |      * @param path the path object for the area that should be drawn; in case
 | 
|---|
| 384 |      * of multipolygons, this can path can be a complex shape with one outer
 | 
|---|
| 385 |      * polygon and one or more inner polygons
 | 
|---|
| 386 |      * @param color The color to fill the area with.
 | 
|---|
| 387 |      * @param fillImage The image to fill the area with. Overrides color.
 | 
|---|
| 388 |      * @param extent if not null, area will be filled partially; specifies, how
 | 
|---|
| 389 |      * far to fill from the boundary towards the center of the area;
 | 
|---|
| 390 |      * if null, area will be filled completely
 | 
|---|
| 391 |      * @param pfClip clipping area for partial fill (only needed for unclosed
 | 
|---|
| 392 |      * polygons)
 | 
|---|
| 393 |      * @param disabled If this should be drawn with a special disabled style.
 | 
|---|
| 394 |      */
 | 
|---|
| 395 |     protected void drawArea(MapViewPath path, Color color,
 | 
|---|
| 396 |             MapImage fillImage, Float extent, Path2D.Double pfClip, boolean disabled) {
 | 
|---|
| 397 |         if (!isOutlineOnly && color.getAlpha() != 0) {
 | 
|---|
| 398 |             Shape area = path;
 | 
|---|
| 399 |             g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
 | 
|---|
| 400 |             if (fillImage == null) {
 | 
|---|
| 401 |                 if (isInactiveMode) {
 | 
|---|
| 402 |                     g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.33f));
 | 
|---|
| 403 |                 }
 | 
|---|
| 404 |                 g.setColor(color);
 | 
|---|
| 405 |                 if (extent == null) {
 | 
|---|
| 406 |                     g.fill(area);
 | 
|---|
| 407 |                 } else {
 | 
|---|
| 408 |                     Shape oldClip = g.getClip();
 | 
|---|
| 409 |                     Shape clip = area;
 | 
|---|
| 410 |                     if (pfClip != null) {
 | 
|---|
| 411 |                         clip = pfClip.createTransformedShape(mapState.getAffineTransform());
 | 
|---|
| 412 |                     }
 | 
|---|
| 413 |                     g.clip(clip);
 | 
|---|
| 414 |                     g.setStroke(new BasicStroke(2 * extent, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 4));
 | 
|---|
| 415 |                     g.draw(area);
 | 
|---|
| 416 |                     g.setClip(oldClip);
 | 
|---|
| 417 |                 }
 | 
|---|
| 418 |             } else {
 | 
|---|
| 419 |                 TexturePaint texture = new TexturePaint(fillImage.getImage(disabled),
 | 
|---|
| 420 |                         new Rectangle(0, 0, fillImage.getWidth(), fillImage.getHeight()));
 | 
|---|
| 421 |                 g.setPaint(texture);
 | 
|---|
| 422 |                 Float alpha = fillImage.getAlphaFloat();
 | 
|---|
| 423 |                 if (!Utils.equalsEpsilon(alpha, 1f)) {
 | 
|---|
| 424 |                     g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
 | 
|---|
| 425 |                 }
 | 
|---|
| 426 |                 if (extent == null) {
 | 
|---|
| 427 |                     g.fill(area);
 | 
|---|
| 428 |                 } else {
 | 
|---|
| 429 |                     Shape oldClip = g.getClip();
 | 
|---|
| 430 |                     BasicStroke stroke = new BasicStroke(2 * extent, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
 | 
|---|
| 431 |                     g.clip(stroke.createStrokedShape(area));
 | 
|---|
| 432 |                     Shape fill = area;
 | 
|---|
| 433 |                     if (pfClip != null) {
 | 
|---|
| 434 |                         fill = pfClip.createTransformedShape(mapState.getAffineTransform());
 | 
|---|
| 435 |                     }
 | 
|---|
| 436 |                     g.fill(fill);
 | 
|---|
| 437 |                     g.setClip(oldClip);
 | 
|---|
| 438 |                 }
 | 
|---|
| 439 |                 g.setPaintMode();
 | 
|---|
| 440 |             }
 | 
|---|
| 441 |             g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
 | 
|---|
| 442 |         }
 | 
|---|
| 443 |     }
 | 
|---|
| 444 | 
 | 
|---|
| 445 |     /**
 | 
|---|
| 446 |      * Draws a multipolygon area.
 | 
|---|
| 447 |      * @param r The multipolygon relation
 | 
|---|
| 448 |      * @param color The color to fill the area with.
 | 
|---|
| 449 |      * @param fillImage The image to fill the area with. Overrides color.
 | 
|---|
| 450 |      * @param extent if not null, area will be filled partially; specifies, how
 | 
|---|
| 451 |      * far to fill from the boundary towards the center of the area;
 | 
|---|
| 452 |      * if null, area will be filled completely
 | 
|---|
| 453 |      * @param extentThreshold if not null, determines if the partial filled should
 | 
|---|
| 454 |      * be replaced by plain fill, when it covers a certain fraction of the total area
 | 
|---|
| 455 |      * @param disabled If this should be drawn with a special disabled style.
 | 
|---|
| 456 |      * @param text Ignored. Use {@link #drawText} instead.
 | 
|---|
| 457 |      * @deprecated use {@link #drawArea(Relation r, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled)}
 | 
|---|
| 458 |      */
 | 
|---|
| 459 |     @Deprecated
 | 
|---|
| 460 |     public void drawArea(Relation r, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled, TextLabel text) {
 | 
|---|
| 461 |         drawArea(r, color, fillImage, extent, extentThreshold, disabled);
 | 
|---|
| 462 |     }
 | 
|---|
| 463 | 
 | 
|---|
| 464 |     /**
 | 
|---|
| 465 |      * Draws a multipolygon area.
 | 
|---|
| 466 |      * @param r The multipolygon relation
 | 
|---|
| 467 |      * @param color The color to fill the area with.
 | 
|---|
| 468 |      * @param fillImage The image to fill the area with. Overrides color.
 | 
|---|
| 469 |      * @param extent if not null, area will be filled partially; specifies, how
 | 
|---|
| 470 |      * far to fill from the boundary towards the center of the area;
 | 
|---|
| 471 |      * if null, area will be filled completely
 | 
|---|
| 472 |      * @param extentThreshold if not null, determines if the partial filled should
 | 
|---|
| 473 |      * be replaced by plain fill, when it covers a certain fraction of the total area
 | 
|---|
| 474 |      * @param disabled If this should be drawn with a special disabled style.
 | 
|---|
| 475 |      * @since 12285
 | 
|---|
| 476 |      */
 | 
|---|
| 477 |     public void drawArea(Relation r, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled) {
 | 
|---|
| 478 |         Multipolygon multipolygon = MultipolygonCache.getInstance().get(r);
 | 
|---|
| 479 |         if (!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) {
 | 
|---|
| 480 |             for (PolyData pd : multipolygon.getCombinedPolygons()) {
 | 
|---|
| 481 |                 if (!isAreaVisible(pd.get())) {
 | 
|---|
| 482 |                     continue;
 | 
|---|
| 483 |                 }
 | 
|---|
| 484 |                 MapViewPath p = new MapViewPath(mapState);
 | 
|---|
| 485 |                 p.appendFromEastNorth(pd.get());
 | 
|---|
| 486 |                 p.setWindingRule(Path2D.WIND_EVEN_ODD);
 | 
|---|
| 487 |                 Path2D.Double pfClip = null;
 | 
|---|
| 488 |                 if (extent != null) {
 | 
|---|
| 489 |                     if (!usePartialFill(pd.getAreaAndPerimeter(null), extent, extentThreshold)) {
 | 
|---|
| 490 |                         extent = null;
 | 
|---|
| 491 |                     } else if (!pd.isClosed()) {
 | 
|---|
| 492 |                         pfClip = getPFClip(pd, extent * scale);
 | 
|---|
| 493 |                     }
 | 
|---|
| 494 |                 }
 | 
|---|
| 495 |                 drawArea(p,
 | 
|---|
| 496 |                         pd.isSelected() ? paintSettings.getRelationSelectedColor(color.getAlpha()) : color,
 | 
|---|
| 497 |                         fillImage, extent, pfClip, disabled);
 | 
|---|
| 498 |             }
 | 
|---|
| 499 |         }
 | 
|---|
| 500 |     }
 | 
|---|
| 501 | 
 | 
|---|
| 502 |     /**
 | 
|---|
| 503 |      * Draws an area defined by a way. They way does not need to be closed, but it should.
 | 
|---|
| 504 |      * @param w The way.
 | 
|---|
| 505 |      * @param color The color to fill the area with.
 | 
|---|
| 506 |      * @param fillImage The image to fill the area with. Overrides color.
 | 
|---|
| 507 |      * @param extent if not null, area will be filled partially; specifies, how
 | 
|---|
| 508 |      * far to fill from the boundary towards the center of the area;
 | 
|---|
| 509 |      * if null, area will be filled completely
 | 
|---|
| 510 |      * @param extentThreshold if not null, determines if the partial filled should
 | 
|---|
| 511 |      * be replaced by plain fill, when it covers a certain fraction of the total area
 | 
|---|
| 512 |      * @param disabled If this should be drawn with a special disabled style.
 | 
|---|
| 513 |      * @param text Ignored. Use {@link #drawText} instead.
 | 
|---|
| 514 |      * @deprecated use {@link #drawArea(Way w, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled)}
 | 
|---|
| 515 |      */
 | 
|---|
| 516 |     @Deprecated
 | 
|---|
| 517 |     public void drawArea(Way w, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled, TextLabel text) {
 | 
|---|
| 518 |         drawArea(w, color, fillImage, extent, extentThreshold, disabled);
 | 
|---|
| 519 |     }
 | 
|---|
| 520 | 
 | 
|---|
| 521 |     /**
 | 
|---|
| 522 |      * Draws an area defined by a way. They way does not need to be closed, but it should.
 | 
|---|
| 523 |      * @param w The way.
 | 
|---|
| 524 |      * @param color The color to fill the area with.
 | 
|---|
| 525 |      * @param fillImage The image to fill the area with. Overrides color.
 | 
|---|
| 526 |      * @param extent if not null, area will be filled partially; specifies, how
 | 
|---|
| 527 |      * far to fill from the boundary towards the center of the area;
 | 
|---|
| 528 |      * if null, area will be filled completely
 | 
|---|
| 529 |      * @param extentThreshold if not null, determines if the partial filled should
 | 
|---|
| 530 |      * be replaced by plain fill, when it covers a certain fraction of the total area
 | 
|---|
| 531 |      * @param disabled If this should be drawn with a special disabled style.
 | 
|---|
| 532 |      * @since 12285
 | 
|---|
| 533 |      */
 | 
|---|
| 534 |     public void drawArea(Way w, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled) {
 | 
|---|
| 535 |         Path2D.Double pfClip = null;
 | 
|---|
| 536 |         if (extent != null) {
 | 
|---|
| 537 |             if (!usePartialFill(Geometry.getAreaAndPerimeter(w.getNodes()), extent, extentThreshold)) {
 | 
|---|
| 538 |                 extent = null;
 | 
|---|
| 539 |             } else if (!w.isClosed()) {
 | 
|---|
| 540 |                 pfClip = getPFClip(w, extent * scale);
 | 
|---|
| 541 |             }
 | 
|---|
| 542 |         }
 | 
|---|
| 543 |         drawArea(getPath(w), color, fillImage, extent, pfClip, disabled);
 | 
|---|
| 544 |     }
 | 
|---|
| 545 | 
 | 
|---|
| 546 |     /**
 | 
|---|
| 547 |      * Determine, if partial fill should be turned off for this object, because
 | 
|---|
| 548 |      * only a small unfilled gap in the center of the area would be left.
 | 
|---|
| 549 |      *
 | 
|---|
| 550 |      * This is used to get a cleaner look for urban regions with many small
 | 
|---|
| 551 |      * areas like buildings, etc.
 | 
|---|
| 552 |      * @param ap the area and the perimeter of the object
 | 
|---|
| 553 |      * @param extent the "width" of partial fill
 | 
|---|
| 554 |      * @param threshold when the partial fill covers that much of the total
 | 
|---|
| 555 |      * area, the partial fill is turned off; can be greater than 100% as the
 | 
|---|
| 556 |      * covered area is estimated as <code>perimeter * extent</code>
 | 
|---|
| 557 |      * @return true, if the partial fill should be used, false otherwise
 | 
|---|
| 558 |      */
 | 
|---|
| 559 |     private boolean usePartialFill(AreaAndPerimeter ap, float extent, Float threshold) {
 | 
|---|
| 560 |         if (threshold == null) return true;
 | 
|---|
| 561 |         return ap.getPerimeter() * extent * scale < threshold * ap.getArea();
 | 
|---|
| 562 |     }
 | 
|---|
| 563 | 
 | 
|---|
| 564 |     /**
 | 
|---|
| 565 |      * Draw a text onto a node
 | 
|---|
| 566 |      * @param n The node to draw the text on
 | 
|---|
| 567 |      * @param bs The text and it's alignment.
 | 
|---|
| 568 |      */
 | 
|---|
| 569 |     public void drawBoxText(Node n, BoxTextElement bs) {
 | 
|---|
| 570 |         if (!isShowNames() || bs == null)
 | 
|---|
| 571 |             return;
 | 
|---|
| 572 | 
 | 
|---|
| 573 |         MapViewPoint p = mapState.getPointFor(n);
 | 
|---|
| 574 |         TextLabel text = bs.text;
 | 
|---|
| 575 |         String s = text.labelCompositionStrategy.compose(n);
 | 
|---|
| 576 |         if (s == null || s.isEmpty()) return;
 | 
|---|
| 577 | 
 | 
|---|
| 578 |         Font defaultFont = g.getFont();
 | 
|---|
| 579 |         g.setFont(text.font);
 | 
|---|
| 580 | 
 | 
|---|
| 581 |         FontRenderContext frc = g.getFontRenderContext();
 | 
|---|
| 582 |         Rectangle2D bounds = text.font.getStringBounds(s, frc);
 | 
|---|
| 583 | 
 | 
|---|
| 584 |         double x = Math.round(p.getInViewX()) + bs.xOffset + bounds.getCenterX();
 | 
|---|
| 585 |         double y = Math.round(p.getInViewY()) + bs.yOffset + bounds.getCenterY();
 | 
|---|
| 586 |         /**
 | 
|---|
| 587 |          *
 | 
|---|
| 588 |          *       left-above __center-above___ right-above
 | 
|---|
| 589 |          *         left-top|                 |right-top
 | 
|---|
| 590 |          *                 |                 |
 | 
|---|
| 591 |          *      left-center|  center-center  |right-center
 | 
|---|
| 592 |          *                 |                 |
 | 
|---|
| 593 |          *      left-bottom|_________________|right-bottom
 | 
|---|
| 594 |          *       left-below   center-below    right-below
 | 
|---|
| 595 |          *
 | 
|---|
| 596 |          */
 | 
|---|
| 597 |         Rectangle box = bs.getBox();
 | 
|---|
| 598 |         if (bs.hAlign == HorizontalTextAlignment.RIGHT) {
 | 
|---|
| 599 |             x += box.x + box.width + 2;
 | 
|---|
| 600 |         } else {
 | 
|---|
| 601 |             int textWidth = (int) bounds.getWidth();
 | 
|---|
| 602 |             if (bs.hAlign == HorizontalTextAlignment.CENTER) {
 | 
|---|
| 603 |                 x -= textWidth / 2;
 | 
|---|
| 604 |             } else if (bs.hAlign == HorizontalTextAlignment.LEFT) {
 | 
|---|
| 605 |                 x -= -box.x + 4 + textWidth;
 | 
|---|
| 606 |             } else throw new AssertionError();
 | 
|---|
| 607 |         }
 | 
|---|
| 608 | 
 | 
|---|
| 609 |         if (bs.vAlign == VerticalTextAlignment.BOTTOM) {
 | 
|---|
| 610 |             y += box.y + box.height;
 | 
|---|
| 611 |         } else {
 | 
|---|
| 612 |             LineMetrics metrics = text.font.getLineMetrics(s, frc);
 | 
|---|
| 613 |             if (bs.vAlign == VerticalTextAlignment.ABOVE) {
 | 
|---|
| 614 |                 y -= -box.y + (int) metrics.getDescent();
 | 
|---|
| 615 |             } else if (bs.vAlign == VerticalTextAlignment.TOP) {
 | 
|---|
| 616 |                 y -= -box.y - (int) metrics.getAscent();
 | 
|---|
| 617 |             } else if (bs.vAlign == VerticalTextAlignment.CENTER) {
 | 
|---|
| 618 |                 y += (int) ((metrics.getAscent() - metrics.getDescent()) / 2);
 | 
|---|
| 619 |             } else if (bs.vAlign == VerticalTextAlignment.BELOW) {
 | 
|---|
| 620 |                 y += box.y + box.height + (int) metrics.getAscent() + 2;
 | 
|---|
| 621 |             } else throw new AssertionError();
 | 
|---|
| 622 |         }
 | 
|---|
| 623 | 
 | 
|---|
| 624 |         displayText(n, text, s, bounds, new MapViewPositionAndRotation(mapState.getForView(x, y), 0));
 | 
|---|
| 625 |         g.setFont(defaultFont);
 | 
|---|
| 626 |     }
 | 
|---|
| 627 | 
 | 
|---|
| 628 |     /**
 | 
|---|
| 629 |      * Draw an image along a way repeatedly.
 | 
|---|
| 630 |      *
 | 
|---|
| 631 |      * @param way the way
 | 
|---|
| 632 |      * @param pattern the image
 | 
|---|
| 633 |      * @param disabled If this should be drawn with a special disabled style.
 | 
|---|
| 634 |      * @param offset offset from the way
 | 
|---|
| 635 |      * @param spacing spacing between two images
 | 
|---|
| 636 |      * @param phase initial spacing
 | 
|---|
| 637 |      * @param align alignment of the image. The top, center or bottom edge can be aligned with the way.
 | 
|---|
| 638 |      */
 | 
|---|
| 639 |     public void drawRepeatImage(Way way, MapImage pattern, boolean disabled, double offset, double spacing, double phase,
 | 
|---|
| 640 |             LineImageAlignment align) {
 | 
|---|
| 641 |         final int imgWidth = pattern.getWidth();
 | 
|---|
| 642 |         final double repeat = imgWidth + spacing;
 | 
|---|
| 643 |         final int imgHeight = pattern.getHeight();
 | 
|---|
| 644 | 
 | 
|---|
| 645 |         int dy1 = (int) ((align.getAlignmentOffset() - .5) * imgHeight);
 | 
|---|
| 646 |         int dy2 = dy1 + imgHeight;
 | 
|---|
| 647 | 
 | 
|---|
| 648 |         OffsetIterator it = new OffsetIterator(mapState, way.getNodes(), offset);
 | 
|---|
| 649 |         MapViewPath path = new MapViewPath(mapState);
 | 
|---|
| 650 |         if (it.hasNext()) {
 | 
|---|
| 651 |             path.moveTo(it.next());
 | 
|---|
| 652 |         }
 | 
|---|
| 653 |         while (it.hasNext()) {
 | 
|---|
| 654 |             path.lineTo(it.next());
 | 
|---|
| 655 |         }
 | 
|---|
| 656 | 
 | 
|---|
| 657 |         double startOffset = computeStartOffset(phase, repeat);
 | 
|---|
| 658 | 
 | 
|---|
| 659 |         BufferedImage image = pattern.getImage(disabled);
 | 
|---|
| 660 | 
 | 
|---|
| 661 |         path.visitClippedLine(repeat, (inLineOffset, start, end, startIsOldEnd) -> {
 | 
|---|
| 662 |             final double segmentLength = start.distanceToInView(end);
 | 
|---|
| 663 |             if (segmentLength < 0.1) {
 | 
|---|
| 664 |                 // avoid odd patterns when zoomed out.
 | 
|---|
| 665 |                 return;
 | 
|---|
| 666 |             }
 | 
|---|
| 667 |             if (segmentLength > repeat * 500) {
 | 
|---|
| 668 |                 // simply skip drawing so many images - something must be wrong.
 | 
|---|
| 669 |                 return;
 | 
|---|
| 670 |             }
 | 
|---|
| 671 |             AffineTransform saveTransform = g.getTransform();
 | 
|---|
| 672 |             g.translate(start.getInViewX(), start.getInViewY());
 | 
|---|
| 673 |             double dx = end.getInViewX() - start.getInViewX();
 | 
|---|
| 674 |             double dy = end.getInViewY() - start.getInViewY();
 | 
|---|
| 675 |             g.rotate(Math.atan2(dy, dx));
 | 
|---|
| 676 | 
 | 
|---|
| 677 |             // The start of the next image
 | 
|---|
| 678 |             // It is shifted by startOffset.
 | 
|---|
| 679 |             double imageStart = -((inLineOffset - startOffset + repeat) % repeat);
 | 
|---|
| 680 | 
 | 
|---|
| 681 |             while (imageStart < segmentLength) {
 | 
|---|
| 682 |                 int x = (int) imageStart;
 | 
|---|
| 683 |                 int sx1 = Math.max(0, -x);
 | 
|---|
| 684 |                 int sx2 = imgWidth - Math.max(0, x + imgWidth - (int) Math.ceil(segmentLength));
 | 
|---|
| 685 |                 g.drawImage(image, x + sx1, dy1, x + sx2, dy2, sx1, 0, sx2, imgHeight, null);
 | 
|---|
| 686 |                 imageStart += repeat;
 | 
|---|
| 687 |             }
 | 
|---|
| 688 | 
 | 
|---|
| 689 |             g.setTransform(saveTransform);
 | 
|---|
| 690 |         });
 | 
|---|
| 691 |     }
 | 
|---|
| 692 | 
 | 
|---|
| 693 |     private static double computeStartOffset(double phase, final double repeat) {
 | 
|---|
| 694 |         double startOffset = phase % repeat;
 | 
|---|
| 695 |         if (startOffset < 0) {
 | 
|---|
| 696 |             startOffset += repeat;
 | 
|---|
| 697 |         }
 | 
|---|
| 698 |         return startOffset;
 | 
|---|
| 699 |     }
 | 
|---|
| 700 | 
 | 
|---|
| 701 |     @Override
 | 
|---|
| 702 |     public void drawNode(Node n, Color color, int size, boolean fill) {
 | 
|---|
| 703 |         if (size <= 0 && !n.isHighlighted())
 | 
|---|
| 704 |             return;
 | 
|---|
| 705 | 
 | 
|---|
| 706 |         MapViewPoint p = mapState.getPointFor(n);
 | 
|---|
| 707 | 
 | 
|---|
| 708 |         if (n.isHighlighted()) {
 | 
|---|
| 709 |             drawPointHighlight(p.getInView(), size);
 | 
|---|
| 710 |         }
 | 
|---|
| 711 | 
 | 
|---|
| 712 |         if (size > 1 && p.isInView()) {
 | 
|---|
| 713 |             int radius = size / 2;
 | 
|---|
| 714 | 
 | 
|---|
| 715 |             if (isInactiveMode || n.isDisabled()) {
 | 
|---|
| 716 |                 g.setColor(inactiveColor);
 | 
|---|
| 717 |             } else {
 | 
|---|
| 718 |                 g.setColor(color);
 | 
|---|
| 719 |             }
 | 
|---|
| 720 |             Rectangle2D rect = new Rectangle2D.Double(p.getInViewX()-radius-1, p.getInViewY()-radius-1, size + 1, size + 1);
 | 
|---|
| 721 |             if (fill) {
 | 
|---|
| 722 |                 g.fill(rect);
 | 
|---|
| 723 |             } else {
 | 
|---|
| 724 |                 g.draw(rect);
 | 
|---|
| 725 |             }
 | 
|---|
| 726 |         }
 | 
|---|
| 727 |     }
 | 
|---|
| 728 | 
 | 
|---|
| 729 |     /**
 | 
|---|
| 730 |      * Draw the icon for a given node.
 | 
|---|
| 731 |      * @param n The node
 | 
|---|
| 732 |      * @param img The icon to draw at the node position
 | 
|---|
| 733 |      * @param disabled {@code} true to render disabled version, {@code false} for the standard version
 | 
|---|
| 734 |      * @param selected {@code} true to render it as selected, {@code false} otherwise
 | 
|---|
| 735 |      * @param member {@code} true to render it as a relation member, {@code false} otherwise
 | 
|---|
| 736 |      * @param theta the angle of rotation in radians
 | 
|---|
| 737 |      */
 | 
|---|
| 738 |     public void drawNodeIcon(Node n, MapImage img, boolean disabled, boolean selected, boolean member, double theta) {
 | 
|---|
| 739 |         MapViewPoint p = mapState.getPointFor(n);
 | 
|---|
| 740 | 
 | 
|---|
| 741 |         int w = img.getWidth();
 | 
|---|
| 742 |         int h = img.getHeight();
 | 
|---|
| 743 |         if (n.isHighlighted()) {
 | 
|---|
| 744 |             drawPointHighlight(p.getInView(), Math.max(w, h));
 | 
|---|
| 745 |         }
 | 
|---|
| 746 | 
 | 
|---|
| 747 |         drawIcon(p, img, disabled, selected, member, theta, (g, r) -> {
 | 
|---|
| 748 |             Color color = getSelectionHintColor(disabled, selected);
 | 
|---|
| 749 |             g.setColor(color);
 | 
|---|
| 750 |             g.draw(r);
 | 
|---|
| 751 |         });
 | 
|---|
| 752 |     }
 | 
|---|
| 753 | 
 | 
|---|
| 754 | 
 | 
|---|
| 755 |     /**
 | 
|---|
| 756 |      * Draw the icon for a given area. Normally, the icon is drawn around the center of the area.
 | 
|---|
| 757 |      * @param osm The primitive to draw the icon for
 | 
|---|
| 758 |      * @param img The icon to draw
 | 
|---|
| 759 |      * @param disabled {@code} true to render disabled version, {@code false} for the standard version
 | 
|---|
| 760 |      * @param selected {@code} true to render it as selected, {@code false} otherwise
 | 
|---|
| 761 |      * @param member {@code} true to render it as a relation member, {@code false} otherwise
 | 
|---|
| 762 |      * @param theta the angle of rotation in radians
 | 
|---|
| 763 |      * @param iconPosition Where to place the icon.
 | 
|---|
| 764 |      * @since 11670
 | 
|---|
| 765 |      */
 | 
|---|
| 766 |     public void drawAreaIcon(OsmPrimitive osm, MapImage img, boolean disabled, boolean selected, boolean member, double theta,
 | 
|---|
| 767 |             PositionForAreaStrategy iconPosition) {
 | 
|---|
| 768 |         Rectangle2D.Double iconRect = new Rectangle2D.Double(-img.getWidth() / 2.0, -img.getHeight() / 2.0, img.getWidth(), img.getHeight());
 | 
|---|
| 769 | 
 | 
|---|
| 770 |         forEachPolygon(osm, path -> {
 | 
|---|
| 771 |             MapViewPositionAndRotation placement = iconPosition.findLabelPlacement(path, iconRect);
 | 
|---|
| 772 |             if (placement == null) {
 | 
|---|
| 773 |                 return;
 | 
|---|
| 774 |             }
 | 
|---|
| 775 |             MapViewPoint p = placement.getPoint();
 | 
|---|
| 776 |             drawIcon(p, img, disabled, selected, member, theta + placement.getRotation(), (g, r) -> {
 | 
|---|
| 777 |                 if (useStrokes) {
 | 
|---|
| 778 |                     g.setStroke(new BasicStroke(2));
 | 
|---|
| 779 |                 }
 | 
|---|
| 780 |                 // only draw a minor highlighting, so that users do not confuse this for a point.
 | 
|---|
| 781 |                 Color color = getSelectionHintColor(disabled, selected);
 | 
|---|
| 782 |                 color = new Color(color.getRed(), color.getGreen(), color.getBlue(), (int) (color.getAlpha() * .2));
 | 
|---|
| 783 |                 g.setColor(color);
 | 
|---|
| 784 |                 g.draw(r);
 | 
|---|
| 785 |             });
 | 
|---|
| 786 |         });
 | 
|---|
| 787 |     }
 | 
|---|
| 788 | 
 | 
|---|
| 789 |     private void drawIcon(MapViewPoint p, MapImage img, boolean disabled, boolean selected, boolean member, double theta,
 | 
|---|
| 790 |             BiConsumer<Graphics2D, Rectangle2D> selectionDrawer) {
 | 
|---|
| 791 |         float alpha = img.getAlphaFloat();
 | 
|---|
| 792 | 
 | 
|---|
| 793 |         Graphics2D temporaryGraphics = (Graphics2D) g.create();
 | 
|---|
| 794 |         if (!Utils.equalsEpsilon(alpha, 1f)) {
 | 
|---|
| 795 |             temporaryGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
 | 
|---|
| 796 |         }
 | 
|---|
| 797 | 
 | 
|---|
| 798 |         double x = Math.round(p.getInViewX());
 | 
|---|
| 799 |         double y = Math.round(p.getInViewY());
 | 
|---|
| 800 |         temporaryGraphics.translate(x, y);
 | 
|---|
| 801 |         temporaryGraphics.rotate(theta);
 | 
|---|
| 802 |         int drawX = -img.getWidth() / 2 + img.offsetX;
 | 
|---|
| 803 |         int drawY = -img.getHeight() / 2 + img.offsetY;
 | 
|---|
| 804 |         temporaryGraphics.drawImage(img.getImage(disabled), drawX, drawY, nc);
 | 
|---|
| 805 |         if (selected || member) {
 | 
|---|
| 806 |             selectionDrawer.accept(temporaryGraphics, new Rectangle2D.Double(drawX - 2, drawY - 2, img.getWidth() + 4, img.getHeight() + 4));
 | 
|---|
| 807 |         }
 | 
|---|
| 808 |     }
 | 
|---|
| 809 | 
 | 
|---|
| 810 |     private Color getSelectionHintColor(boolean disabled, boolean selected) {
 | 
|---|
| 811 |         Color color;
 | 
|---|
| 812 |         if (disabled) {
 | 
|---|
| 813 |             color = inactiveColor;
 | 
|---|
| 814 |         } else if (selected) {
 | 
|---|
| 815 |             color = selectedColor;
 | 
|---|
| 816 |         } else {
 | 
|---|
| 817 |             color = relationSelectedColor;
 | 
|---|
| 818 |         }
 | 
|---|
| 819 |         return color;
 | 
|---|
| 820 |     }
 | 
|---|
| 821 | 
 | 
|---|
| 822 |     /**
 | 
|---|
| 823 |      * Draw the symbol and possibly a highlight marking on a given node.
 | 
|---|
| 824 |      * @param n The position to draw the symbol on
 | 
|---|
| 825 |      * @param s The symbol to draw
 | 
|---|
| 826 |      * @param fillColor The color to fill the symbol with
 | 
|---|
| 827 |      * @param strokeColor The color to use for the outer corner of the symbol
 | 
|---|
| 828 |      */
 | 
|---|
| 829 |     public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) {
 | 
|---|
| 830 |         MapViewPoint p = mapState.getPointFor(n);
 | 
|---|
| 831 | 
 | 
|---|
| 832 |         if (n.isHighlighted()) {
 | 
|---|
| 833 |             drawPointHighlight(p.getInView(), s.size);
 | 
|---|
| 834 |         }
 | 
|---|
| 835 | 
 | 
|---|
| 836 |         if (fillColor != null || strokeColor != null) {
 | 
|---|
| 837 |             Shape shape = s.buildShapeAround(p.getInViewX(), p.getInViewY());
 | 
|---|
| 838 | 
 | 
|---|
| 839 |             if (fillColor != null) {
 | 
|---|
| 840 |                 g.setColor(fillColor);
 | 
|---|
| 841 |                 g.fill(shape);
 | 
|---|
| 842 |             }
 | 
|---|
| 843 |             if (s.stroke != null) {
 | 
|---|
| 844 |                 g.setStroke(s.stroke);
 | 
|---|
| 845 |                 g.setColor(strokeColor);
 | 
|---|
| 846 |                 g.draw(shape);
 | 
|---|
| 847 |                 g.setStroke(new BasicStroke());
 | 
|---|
| 848 |             }
 | 
|---|
| 849 |         }
 | 
|---|
| 850 |     }
 | 
|---|
| 851 | 
 | 
|---|
| 852 |     /**
 | 
|---|
| 853 |      * Draw a number of the order of the two consecutive nodes within the
 | 
|---|
| 854 |      * parents way
 | 
|---|
| 855 |      *
 | 
|---|
| 856 |      * @param n1 First node of the way segment.
 | 
|---|
| 857 |      * @param n2 Second node of the way segment.
 | 
|---|
| 858 |      * @param orderNumber The number of the segment in the way.
 | 
|---|
| 859 |      * @param clr The color to use for drawing the text.
 | 
|---|
| 860 |      */
 | 
|---|
| 861 |     public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) {
 | 
|---|
| 862 |         MapViewPoint p1 = mapState.getPointFor(n1);
 | 
|---|
| 863 |         MapViewPoint p2 = mapState.getPointFor(n2);
 | 
|---|
| 864 |         drawOrderNumber(p1, p2, orderNumber, clr);
 | 
|---|
| 865 |     }
 | 
|---|
| 866 | 
 | 
|---|
| 867 |     /**
 | 
|---|
| 868 |      * highlights a given GeneralPath using the settings from BasicStroke to match the line's
 | 
|---|
| 869 |      * style. Width of the highlight can be changed by user preferences
 | 
|---|
| 870 |      * @param path path to draw
 | 
|---|
| 871 |      * @param line line style
 | 
|---|
| 872 |      */
 | 
|---|
| 873 |     private void drawPathHighlight(MapViewPath path, BasicStroke line) {
 | 
|---|
| 874 |         if (path == null)
 | 
|---|
| 875 |             return;
 | 
|---|
| 876 |         g.setColor(highlightColorTransparent);
 | 
|---|
| 877 |         float w = line.getLineWidth() + HIGHLIGHT_LINE_WIDTH.get();
 | 
|---|
| 878 |         if (useWiderHighlight) {
 | 
|---|
| 879 |             w += WIDER_HIGHLIGHT.get();
 | 
|---|
| 880 |         }
 | 
|---|
| 881 |         int step = Math.max(HIGHLIGHT_STEP.get(), 1);
 | 
|---|
| 882 |         while (w >= line.getLineWidth()) {
 | 
|---|
| 883 |             g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit()));
 | 
|---|
| 884 |             g.draw(path);
 | 
|---|
| 885 |             w -= step;
 | 
|---|
| 886 |         }
 | 
|---|
| 887 |     }
 | 
|---|
| 888 | 
 | 
|---|
| 889 |     /**
 | 
|---|
| 890 |      * highlights a given point by drawing a rounded rectangle around it. Give the
 | 
|---|
| 891 |      * size of the object you want to be highlighted, width is added automatically.
 | 
|---|
| 892 |      * @param p point
 | 
|---|
| 893 |      * @param size highlight size
 | 
|---|
| 894 |      */
 | 
|---|
| 895 |     private void drawPointHighlight(Point2D p, int size) {
 | 
|---|
| 896 |         g.setColor(highlightColorTransparent);
 | 
|---|
| 897 |         int s = size + HIGHLIGHT_POINT_RADIUS.get();
 | 
|---|
| 898 |         if (useWiderHighlight) {
 | 
|---|
| 899 |             s += WIDER_HIGHLIGHT.get();
 | 
|---|
| 900 |         }
 | 
|---|
| 901 |         int step = Math.max(HIGHLIGHT_STEP.get(), 1);
 | 
|---|
| 902 |         while (s >= size) {
 | 
|---|
| 903 |             int r = (int) Math.floor(s/2d);
 | 
|---|
| 904 |             g.fill(new RoundRectangle2D.Double(p.getX()-r, p.getY()-r, s, s, r, r));
 | 
|---|
| 905 |             s -= step;
 | 
|---|
| 906 |         }
 | 
|---|
| 907 |     }
 | 
|---|
| 908 | 
 | 
|---|
| 909 |     public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) {
 | 
|---|
| 910 |         // rotate image with direction last node in from to, and scale down image to 16*16 pixels
 | 
|---|
| 911 |         Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16));
 | 
|---|
| 912 |         int w = smallImg.getWidth(null), h = smallImg.getHeight(null);
 | 
|---|
| 913 |         g.drawImage(smallImg, (int) (pVia.x+vx+vx2)-w/2, (int) (pVia.y+vy+vy2)-h/2, nc);
 | 
|---|
| 914 | 
 | 
|---|
| 915 |         if (selected) {
 | 
|---|
| 916 |             g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor);
 | 
|---|
| 917 |             g.drawRect((int) (pVia.x+vx+vx2)-w/2-2, (int) (pVia.y+vy+vy2)-h/2-2, w+4, h+4);
 | 
|---|
| 918 |         }
 | 
|---|
| 919 |     }
 | 
|---|
| 920 | 
 | 
|---|
| 921 |     /**
 | 
|---|
| 922 |      * Draw a turn restriction
 | 
|---|
| 923 |      * @param r The turn restriction relation
 | 
|---|
| 924 |      * @param icon The icon to draw at the turn point
 | 
|---|
| 925 |      * @param disabled draw using disabled style
 | 
|---|
| 926 |      */
 | 
|---|
| 927 |     public void drawRestriction(Relation r, MapImage icon, boolean disabled) {
 | 
|---|
| 928 |         Way fromWay = null;
 | 
|---|
| 929 |         Way toWay = null;
 | 
|---|
| 930 |         OsmPrimitive via = null;
 | 
|---|
| 931 | 
 | 
|---|
| 932 |         /* find the "from", "via" and "to" elements */
 | 
|---|
| 933 |         for (RelationMember m : r.getMembers()) {
 | 
|---|
| 934 |             if (m.getMember().isIncomplete())
 | 
|---|
| 935 |                 return;
 | 
|---|
| 936 |             else {
 | 
|---|
| 937 |                 if (m.isWay()) {
 | 
|---|
| 938 |                     Way w = m.getWay();
 | 
|---|
| 939 |                     if (w.getNodesCount() < 2) {
 | 
|---|
| 940 |                         continue;
 | 
|---|
| 941 |                     }
 | 
|---|
| 942 | 
 | 
|---|
| 943 |                     switch(m.getRole()) {
 | 
|---|
| 944 |                     case "from":
 | 
|---|
| 945 |                         if (fromWay == null) {
 | 
|---|
| 946 |                             fromWay = w;
 | 
|---|
| 947 |                         }
 | 
|---|
| 948 |                         break;
 | 
|---|
| 949 |                     case "to":
 | 
|---|
| 950 |                         if (toWay == null) {
 | 
|---|
| 951 |                             toWay = w;
 | 
|---|
| 952 |                         }
 | 
|---|
| 953 |                         break;
 | 
|---|
| 954 |                     case "via":
 | 
|---|
| 955 |                         if (via == null) {
 | 
|---|
| 956 |                             via = w;
 | 
|---|
| 957 |                         }
 | 
|---|
| 958 |                         break;
 | 
|---|
| 959 |                     default: // Do nothing
 | 
|---|
| 960 |                     }
 | 
|---|
| 961 |                 } else if (m.isNode()) {
 | 
|---|
| 962 |                     Node n = m.getNode();
 | 
|---|
| 963 |                     if (via == null && "via".equals(m.getRole())) {
 | 
|---|
| 964 |                         via = n;
 | 
|---|
| 965 |                     }
 | 
|---|
| 966 |                 }
 | 
|---|
| 967 |             }
 | 
|---|
| 968 |         }
 | 
|---|
| 969 | 
 | 
|---|
| 970 |         if (fromWay == null || toWay == null || via == null)
 | 
|---|
| 971 |             return;
 | 
|---|
| 972 | 
 | 
|---|
| 973 |         Node viaNode;
 | 
|---|
| 974 |         if (via instanceof Node) {
 | 
|---|
| 975 |             viaNode = (Node) via;
 | 
|---|
| 976 |             if (!fromWay.isFirstLastNode(viaNode))
 | 
|---|
| 977 |                 return;
 | 
|---|
| 978 |         } else {
 | 
|---|
| 979 |             Way viaWay = (Way) via;
 | 
|---|
| 980 |             Node firstNode = viaWay.firstNode();
 | 
|---|
| 981 |             Node lastNode = viaWay.lastNode();
 | 
|---|
| 982 |             Boolean onewayvia = Boolean.FALSE;
 | 
|---|
| 983 | 
 | 
|---|
| 984 |             String onewayviastr = viaWay.get("oneway");
 | 
|---|
| 985 |             if (onewayviastr != null) {
 | 
|---|
| 986 |                 if ("-1".equals(onewayviastr)) {
 | 
|---|
| 987 |                     onewayvia = Boolean.TRUE;
 | 
|---|
| 988 |                     Node tmp = firstNode;
 | 
|---|
| 989 |                     firstNode = lastNode;
 | 
|---|
| 990 |                     lastNode = tmp;
 | 
|---|
| 991 |                 } else {
 | 
|---|
| 992 |                     onewayvia = Optional.ofNullable(OsmUtils.getOsmBoolean(onewayviastr)).orElse(Boolean.FALSE);
 | 
|---|
| 993 |                 }
 | 
|---|
| 994 |             }
 | 
|---|
| 995 | 
 | 
|---|
| 996 |             if (fromWay.isFirstLastNode(firstNode)) {
 | 
|---|
| 997 |                 viaNode = firstNode;
 | 
|---|
| 998 |             } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
 | 
|---|
| 999 |                 viaNode = lastNode;
 | 
|---|
| 1000 |             } else
 | 
|---|
| 1001 |                 return;
 | 
|---|
| 1002 |         }
 | 
|---|
| 1003 | 
 | 
|---|
| 1004 |         /* find the "direct" nodes before the via node */
 | 
|---|
| 1005 |         Node fromNode;
 | 
|---|
| 1006 |         if (fromWay.firstNode() == via) {
 | 
|---|
| 1007 |             fromNode = fromWay.getNode(1);
 | 
|---|
| 1008 |         } else {
 | 
|---|
| 1009 |             fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
 | 
|---|
| 1010 |         }
 | 
|---|
| 1011 | 
 | 
|---|
| 1012 |         Point pFrom = nc.getPoint(fromNode);
 | 
|---|
| 1013 |         Point pVia = nc.getPoint(viaNode);
 | 
|---|
| 1014 | 
 | 
|---|
| 1015 |         /* starting from via, go back the "from" way a few pixels
 | 
|---|
| 1016 |            (calculate the vector vx/vy with the specified length and the direction
 | 
|---|
| 1017 |            away from the "via" node along the first segment of the "from" way)
 | 
|---|
| 1018 |          */
 | 
|---|
| 1019 |         double distanceFromVia = 14;
 | 
|---|
| 1020 |         double dx = pFrom.x >= pVia.x ? pFrom.x - pVia.x : pVia.x - pFrom.x;
 | 
|---|
| 1021 |         double dy = pFrom.y >= pVia.y ? pFrom.y - pVia.y : pVia.y - pFrom.y;
 | 
|---|
| 1022 | 
 | 
|---|
| 1023 |         double fromAngle;
 | 
|---|
| 1024 |         if (dx == 0) {
 | 
|---|
| 1025 |             fromAngle = Math.PI/2;
 | 
|---|
| 1026 |         } else {
 | 
|---|
| 1027 |             fromAngle = Math.atan(dy / dx);
 | 
|---|
| 1028 |         }
 | 
|---|
| 1029 |         double fromAngleDeg = Utils.toDegrees(fromAngle);
 | 
|---|
| 1030 | 
 | 
|---|
| 1031 |         double vx = distanceFromVia * Math.cos(fromAngle);
 | 
|---|
| 1032 |         double vy = distanceFromVia * Math.sin(fromAngle);
 | 
|---|
| 1033 | 
 | 
|---|
| 1034 |         if (pFrom.x < pVia.x) {
 | 
|---|
| 1035 |             vx = -vx;
 | 
|---|
| 1036 |         }
 | 
|---|
| 1037 |         if (pFrom.y < pVia.y) {
 | 
|---|
| 1038 |             vy = -vy;
 | 
|---|
| 1039 |         }
 | 
|---|
| 1040 | 
 | 
|---|
| 1041 |         /* go a few pixels away from the way (in a right angle)
 | 
|---|
| 1042 |            (calculate the vx2/vy2 vector with the specified length and the direction
 | 
|---|
| 1043 |            90degrees away from the first segment of the "from" way)
 | 
|---|
| 1044 |          */
 | 
|---|
| 1045 |         double distanceFromWay = 10;
 | 
|---|
| 1046 |         double vx2 = 0;
 | 
|---|
| 1047 |         double vy2 = 0;
 | 
|---|
| 1048 |         double iconAngle = 0;
 | 
|---|
| 1049 | 
 | 
|---|
| 1050 |         if (pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
 | 
|---|
| 1051 |             if (!leftHandTraffic) {
 | 
|---|
| 1052 |                 vx2 = distanceFromWay * Math.cos(Utils.toRadians(fromAngleDeg - 90));
 | 
|---|
| 1053 |                 vy2 = distanceFromWay * Math.sin(Utils.toRadians(fromAngleDeg - 90));
 | 
|---|
| 1054 |             } else {
 | 
|---|
| 1055 |                 vx2 = distanceFromWay * Math.cos(Utils.toRadians(fromAngleDeg + 90));
 | 
|---|
| 1056 |                 vy2 = distanceFromWay * Math.sin(Utils.toRadians(fromAngleDeg + 90));
 | 
|---|
| 1057 |             }
 | 
|---|
| 1058 |             iconAngle = 270+fromAngleDeg;
 | 
|---|
| 1059 |         }
 | 
|---|
| 1060 |         if (pFrom.x < pVia.x && pFrom.y >= pVia.y) {
 | 
|---|
| 1061 |             if (!leftHandTraffic) {
 | 
|---|
| 1062 |                 vx2 = distanceFromWay * Math.sin(Utils.toRadians(fromAngleDeg));
 | 
|---|
| 1063 |                 vy2 = distanceFromWay * Math.cos(Utils.toRadians(fromAngleDeg));
 | 
|---|
| 1064 |             } else {
 | 
|---|
| 1065 |                 vx2 = distanceFromWay * Math.sin(Utils.toRadians(fromAngleDeg + 180));
 | 
|---|
| 1066 |                 vy2 = distanceFromWay * Math.cos(Utils.toRadians(fromAngleDeg + 180));
 | 
|---|
| 1067 |             }
 | 
|---|
| 1068 |             iconAngle = 90-fromAngleDeg;
 | 
|---|
| 1069 |         }
 | 
|---|
| 1070 |         if (pFrom.x < pVia.x && pFrom.y < pVia.y) {
 | 
|---|
| 1071 |             if (!leftHandTraffic) {
 | 
|---|
| 1072 |                 vx2 = distanceFromWay * Math.cos(Utils.toRadians(fromAngleDeg + 90));
 | 
|---|
| 1073 |                 vy2 = distanceFromWay * Math.sin(Utils.toRadians(fromAngleDeg + 90));
 | 
|---|
| 1074 |             } else {
 | 
|---|
| 1075 |                 vx2 = distanceFromWay * Math.cos(Utils.toRadians(fromAngleDeg - 90));
 | 
|---|
| 1076 |                 vy2 = distanceFromWay * Math.sin(Utils.toRadians(fromAngleDeg - 90));
 | 
|---|
| 1077 |             }
 | 
|---|
| 1078 |             iconAngle = 90+fromAngleDeg;
 | 
|---|
| 1079 |         }
 | 
|---|
| 1080 |         if (pFrom.x >= pVia.x && pFrom.y < pVia.y) {
 | 
|---|
| 1081 |             if (!leftHandTraffic) {
 | 
|---|
| 1082 |                 vx2 = distanceFromWay * Math.sin(Utils.toRadians(fromAngleDeg + 180));
 | 
|---|
| 1083 |                 vy2 = distanceFromWay * Math.cos(Utils.toRadians(fromAngleDeg + 180));
 | 
|---|
| 1084 |             } else {
 | 
|---|
| 1085 |                 vx2 = distanceFromWay * Math.sin(Utils.toRadians(fromAngleDeg));
 | 
|---|
| 1086 |                 vy2 = distanceFromWay * Math.cos(Utils.toRadians(fromAngleDeg));
 | 
|---|
| 1087 |             }
 | 
|---|
| 1088 |             iconAngle = 270-fromAngleDeg;
 | 
|---|
| 1089 |         }
 | 
|---|
| 1090 | 
 | 
|---|
| 1091 |         drawRestriction(icon.getImage(disabled),
 | 
|---|
| 1092 |                 pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
 | 
|---|
| 1093 |     }
 | 
|---|
| 1094 | 
 | 
|---|
| 1095 |     /**
 | 
|---|
| 1096 |      * Draws a text for the given primitive
 | 
|---|
| 1097 |      * @param osm The primitive to draw the text for
 | 
|---|
| 1098 |      * @param text The text definition (font/position/.../text content) to draw
 | 
|---|
| 1099 |      * @param labelPositionStrategy The position of the text
 | 
|---|
| 1100 |      * @since 11722
 | 
|---|
| 1101 |      */
 | 
|---|
| 1102 |     public void drawText(OsmPrimitive osm, TextLabel text, PositionForAreaStrategy labelPositionStrategy) {
 | 
|---|
| 1103 |         if (!isShowNames()) {
 | 
|---|
| 1104 |             return;
 | 
|---|
| 1105 |         }
 | 
|---|
| 1106 |         String name = text.getString(osm);
 | 
|---|
| 1107 |         if (name == null || name.isEmpty()) {
 | 
|---|
| 1108 |             return;
 | 
|---|
| 1109 |         }
 | 
|---|
| 1110 | 
 | 
|---|
| 1111 |         FontMetrics fontMetrics = g.getFontMetrics(text.font); // if slow, use cache
 | 
|---|
| 1112 |         Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
 | 
|---|
| 1113 | 
 | 
|---|
| 1114 |         Font defaultFont = g.getFont();
 | 
|---|
| 1115 |         forEachPolygon(osm, path -> {
 | 
|---|
| 1116 |             //TODO: Ignore areas that are out of bounds.
 | 
|---|
| 1117 |             PositionForAreaStrategy position = labelPositionStrategy;
 | 
|---|
| 1118 |             MapViewPositionAndRotation center = position.findLabelPlacement(path, nb);
 | 
|---|
| 1119 |             if (center != null) {
 | 
|---|
| 1120 |                 displayText(osm, text, name, nb, center);
 | 
|---|
| 1121 |             } else if (position.supportsGlyphVector()) {
 | 
|---|
| 1122 |                 List<GlyphVector> gvs = Utils.getGlyphVectorsBidi(name, text.font, g.getFontRenderContext());
 | 
|---|
| 1123 | 
 | 
|---|
| 1124 |                 List<GlyphVector> translatedGvs = position.generateGlyphVectors(path, nb, gvs, isGlyphVectorDoubleTranslationBug(text.font));
 | 
|---|
| 1125 |                 displayText(() -> translatedGvs.forEach(gv -> g.drawGlyphVector(gv, 0, 0)),
 | 
|---|
| 1126 |                         () -> translatedGvs.stream().collect(
 | 
|---|
| 1127 |                                 Path2D.Double::new,
 | 
|---|
| 1128 |                                 (p, gv) -> p.append(gv.getOutline(0, 0), false),
 | 
|---|
| 1129 |                                 (p1, p2) -> p1.append(p2, false)),
 | 
|---|
| 1130 |                         osm.isDisabled(), text);
 | 
|---|
| 1131 |             } else if (Main.isTraceEnabled()) {
 | 
|---|
| 1132 |                 Main.trace("Couldn't find a correct label placement for " + osm + " / " + name);
 | 
|---|
| 1133 |             }
 | 
|---|
| 1134 |         });
 | 
|---|
| 1135 |         g.setFont(defaultFont);
 | 
|---|
| 1136 |     }
 | 
|---|
| 1137 | 
 | 
|---|
| 1138 |     private void displayText(OsmPrimitive osm, TextLabel text, String name, Rectangle2D nb,
 | 
|---|
| 1139 |             MapViewPositionAndRotation center) {
 | 
|---|
| 1140 |         AffineTransform at = new AffineTransform();
 | 
|---|
| 1141 |         if (Math.abs(center.getRotation()) < .01) {
 | 
|---|
| 1142 |             // Explicitly no rotation: move to full pixels.
 | 
|---|
| 1143 |             at.setToTranslation(Math.round(center.getPoint().getInViewX() - nb.getCenterX()),
 | 
|---|
| 1144 |                     Math.round(center.getPoint().getInViewY() - nb.getCenterY()));
 | 
|---|
| 1145 |         } else {
 | 
|---|
| 1146 |             at.setToTranslation(center.getPoint().getInViewX(), center.getPoint().getInViewY());
 | 
|---|
| 1147 |             at.rotate(center.getRotation());
 | 
|---|
| 1148 |             at.translate(-nb.getCenterX(), -nb.getCenterY());
 | 
|---|
| 1149 |         }
 | 
|---|
| 1150 |         displayText(() -> {
 | 
|---|
| 1151 |             AffineTransform defaultTransform = g.getTransform();
 | 
|---|
| 1152 |             g.setTransform(at);
 | 
|---|
| 1153 |             g.setFont(text.font);
 | 
|---|
| 1154 |             g.drawString(name, 0, 0);
 | 
|---|
| 1155 |             g.setTransform(defaultTransform);
 | 
|---|
| 1156 |         }, () -> {
 | 
|---|
| 1157 |             FontRenderContext frc = g.getFontRenderContext();
 | 
|---|
| 1158 |             TextLayout tl = new TextLayout(name, text.font, frc);
 | 
|---|
| 1159 |             return tl.getOutline(at);
 | 
|---|
| 1160 |         }, osm.isDisabled(), text);
 | 
|---|
| 1161 |     }
 | 
|---|
| 1162 | 
 | 
|---|
| 1163 |     /**
 | 
|---|
| 1164 |      * Displays text at specified position including its halo, if applicable.
 | 
|---|
| 1165 |      *
 | 
|---|
| 1166 |      * @param fill The function that fills the text
 | 
|---|
| 1167 |      * @param outline The function to draw the outline
 | 
|---|
| 1168 |      * @param disabled {@code true} if element is disabled (filtered out)
 | 
|---|
| 1169 |      * @param text text style to use
 | 
|---|
| 1170 |      */
 | 
|---|
| 1171 |     private void displayText(Runnable fill, Supplier<Shape> outline, boolean disabled, TextLabel text) {
 | 
|---|
| 1172 |         if (isInactiveMode || disabled) {
 | 
|---|
| 1173 |             g.setColor(inactiveColor);
 | 
|---|
| 1174 |             fill.run();
 | 
|---|
| 1175 |         } else if (text.haloRadius != null) {
 | 
|---|
| 1176 |             g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
 | 
|---|
| 1177 |             g.setColor(text.haloColor);
 | 
|---|
| 1178 |             Shape textOutline = outline.get();
 | 
|---|
| 1179 |             g.draw(textOutline);
 | 
|---|
| 1180 |             g.setStroke(new BasicStroke());
 | 
|---|
| 1181 |             g.setColor(text.color);
 | 
|---|
| 1182 |             g.fill(textOutline);
 | 
|---|
| 1183 |         } else {
 | 
|---|
| 1184 |             g.setColor(text.color);
 | 
|---|
| 1185 |             fill.run();
 | 
|---|
| 1186 |         }
 | 
|---|
| 1187 |     }
 | 
|---|
| 1188 | 
 | 
|---|
| 1189 |     /**
 | 
|---|
| 1190 |      * Calls a consumer for each path of the area shape-
 | 
|---|
| 1191 |      * @param osm A way or a multipolygon
 | 
|---|
| 1192 |      * @param consumer The consumer to call.
 | 
|---|
| 1193 |      */
 | 
|---|
| 1194 |     private void forEachPolygon(OsmPrimitive osm, Consumer<MapViewPath> consumer) {
 | 
|---|
| 1195 |         if (osm instanceof Way) {
 | 
|---|
| 1196 |             consumer.accept(getPath((Way) osm));
 | 
|---|
| 1197 |         } else if (osm instanceof Relation) {
 | 
|---|
| 1198 |             Multipolygon multipolygon = MultipolygonCache.getInstance().get((Relation) osm);
 | 
|---|
| 1199 |             if (!multipolygon.getOuterWays().isEmpty()) {
 | 
|---|
| 1200 |                 for (PolyData pd : multipolygon.getCombinedPolygons()) {
 | 
|---|
| 1201 |                     MapViewPath path = new MapViewPath(mapState);
 | 
|---|
| 1202 |                     path.appendFromEastNorth(pd.get());
 | 
|---|
| 1203 |                     path.setWindingRule(MapViewPath.WIND_EVEN_ODD);
 | 
|---|
| 1204 |                     consumer.accept(path);
 | 
|---|
| 1205 |                 }
 | 
|---|
| 1206 |             }
 | 
|---|
| 1207 |         }
 | 
|---|
| 1208 |     }
 | 
|---|
| 1209 | 
 | 
|---|
| 1210 |     /**
 | 
|---|
| 1211 |      * draw way. This method allows for two draw styles (line using color, dashes using dashedColor) to be passed.
 | 
|---|
| 1212 |      * @param way The way to draw
 | 
|---|
| 1213 |      * @param color The base color to draw the way in
 | 
|---|
| 1214 |      * @param line The line style to use. This is drawn using color.
 | 
|---|
| 1215 |      * @param dashes The dash style to use. This is drawn using dashedColor. <code>null</code> if unused.
 | 
|---|
| 1216 |      * @param dashedColor The color of the dashes.
 | 
|---|
| 1217 |      * @param offset The offset
 | 
|---|
| 1218 |      * @param showOrientation show arrows that indicate the technical orientation of
 | 
|---|
| 1219 |      *              the way (defined by order of nodes)
 | 
|---|
| 1220 |      * @param showHeadArrowOnly True if only the arrow at the end of the line but not those on the segments should be displayed.
 | 
|---|
| 1221 |      * @param showOneway show symbols that indicate the direction of the feature,
 | 
|---|
| 1222 |      *              e.g. oneway street or waterway
 | 
|---|
| 1223 |      * @param onewayReversed for oneway=-1 and similar
 | 
|---|
| 1224 |      */
 | 
|---|
| 1225 |     public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset,
 | 
|---|
| 1226 |             boolean showOrientation, boolean showHeadArrowOnly,
 | 
|---|
| 1227 |             boolean showOneway, boolean onewayReversed) {
 | 
|---|
| 1228 | 
 | 
|---|
| 1229 |         MapViewPath path = new MapViewPath(mapState);
 | 
|---|
| 1230 |         MapViewPath orientationArrows = showOrientation ? new MapViewPath(mapState) : null;
 | 
|---|
| 1231 |         MapViewPath onewayArrows;
 | 
|---|
| 1232 |         MapViewPath onewayArrowsCasing;
 | 
|---|
| 1233 |         Rectangle bounds = g.getClipBounds();
 | 
|---|
| 1234 |         if (bounds != null) {
 | 
|---|
| 1235 |             // avoid arrow heads at the border
 | 
|---|
| 1236 |             bounds.grow(100, 100);
 | 
|---|
| 1237 |         }
 | 
|---|
| 1238 | 
 | 
|---|
| 1239 |         List<Node> wayNodes = way.getNodes();
 | 
|---|
| 1240 |         if (wayNodes.size() < 2) return;
 | 
|---|
| 1241 | 
 | 
|---|
| 1242 |         // only highlight the segment if the way itself is not highlighted
 | 
|---|
| 1243 |         if (!way.isHighlighted() && highlightWaySegments != null) {
 | 
|---|
| 1244 |             MapViewPath highlightSegs = null;
 | 
|---|
| 1245 |             for (WaySegment ws : highlightWaySegments) {
 | 
|---|
| 1246 |                 if (ws.way != way || ws.lowerIndex < offset) {
 | 
|---|
| 1247 |                     continue;
 | 
|---|
| 1248 |                 }
 | 
|---|
| 1249 |                 if (highlightSegs == null) {
 | 
|---|
| 1250 |                     highlightSegs = new MapViewPath(mapState);
 | 
|---|
| 1251 |                 }
 | 
|---|
| 1252 | 
 | 
|---|
| 1253 |                 highlightSegs.moveTo(ws.getFirstNode());
 | 
|---|
| 1254 |                 highlightSegs.lineTo(ws.getSecondNode());
 | 
|---|
| 1255 |             }
 | 
|---|
| 1256 | 
 | 
|---|
| 1257 |             drawPathHighlight(highlightSegs, line);
 | 
|---|
| 1258 |         }
 | 
|---|
| 1259 | 
 | 
|---|
| 1260 |         MapViewPoint lastPoint = null;
 | 
|---|
| 1261 |         Iterator<MapViewPoint> it = new OffsetIterator(mapState, wayNodes, offset);
 | 
|---|
| 1262 |         boolean initialMoveToNeeded = true;
 | 
|---|
| 1263 |         ArrowPaintHelper drawArrowHelper = null;
 | 
|---|
| 1264 |         if (showOrientation) {
 | 
|---|
| 1265 |             drawArrowHelper = new ArrowPaintHelper(PHI, 10 + line.getLineWidth());
 | 
|---|
| 1266 |         }
 | 
|---|
| 1267 |         while (it.hasNext()) {
 | 
|---|
| 1268 |             MapViewPoint p = it.next();
 | 
|---|
| 1269 |             if (lastPoint != null) {
 | 
|---|
| 1270 |                 MapViewPoint p1 = lastPoint;
 | 
|---|
| 1271 |                 MapViewPoint p2 = p;
 | 
|---|
| 1272 | 
 | 
|---|
| 1273 |                 if (initialMoveToNeeded) {
 | 
|---|
| 1274 |                     initialMoveToNeeded = false;
 | 
|---|
| 1275 |                     path.moveTo(p1);
 | 
|---|
| 1276 |                 }
 | 
|---|
| 1277 |                 path.lineTo(p2);
 | 
|---|
| 1278 | 
 | 
|---|
| 1279 |                 /* draw arrow */
 | 
|---|
| 1280 |                 if (drawArrowHelper != null) {
 | 
|---|
| 1281 |                     boolean drawArrow;
 | 
|---|
| 1282 |                     // always draw last arrow - no matter how short the segment is
 | 
|---|
| 1283 |                     drawArrow = !it.hasNext();
 | 
|---|
| 1284 |                     if (!showHeadArrowOnly) {
 | 
|---|
| 1285 |                         // draw arrows in between only if there is enough space
 | 
|---|
| 1286 |                         drawArrow = drawArrow || p1.distanceToInView(p2) > drawArrowHelper.getOnLineLength() * 1.3;
 | 
|---|
| 1287 |                     }
 | 
|---|
| 1288 |                     if (drawArrow) {
 | 
|---|
| 1289 |                         drawArrowHelper.paintArrowAt(orientationArrows, p2, p1);
 | 
|---|
| 1290 |                     }
 | 
|---|
| 1291 |                 }
 | 
|---|
| 1292 |             }
 | 
|---|
| 1293 |             lastPoint = p;
 | 
|---|
| 1294 |         }
 | 
|---|
| 1295 |         if (showOneway) {
 | 
|---|
| 1296 |             onewayArrows = new MapViewPath(mapState);
 | 
|---|
| 1297 |             onewayArrowsCasing = new MapViewPath(mapState);
 | 
|---|
| 1298 |             double interval = 60;
 | 
|---|
| 1299 | 
 | 
|---|
| 1300 |             path.visitClippedLine(60, (inLineOffset, start, end, startIsOldEnd) -> {
 | 
|---|
| 1301 |                 double segmentLength = start.distanceToInView(end);
 | 
|---|
| 1302 |                 if (segmentLength > 0.001) {
 | 
|---|
| 1303 |                     final double nx = (end.getInViewX() - start.getInViewX()) / segmentLength;
 | 
|---|
| 1304 |                     final double ny = (end.getInViewY() - start.getInViewY()) / segmentLength;
 | 
|---|
| 1305 | 
 | 
|---|
| 1306 |                     // distance from p1
 | 
|---|
| 1307 |                     double dist = interval - (inLineOffset % interval);
 | 
|---|
| 1308 | 
 | 
|---|
| 1309 |                     while (dist < segmentLength) {
 | 
|---|
| 1310 |                         appendOnewayPath(onewayReversed, start, nx, ny, dist, 3d, onewayArrowsCasing);
 | 
|---|
| 1311 |                         appendOnewayPath(onewayReversed, start, nx, ny, dist, 2d, onewayArrows);
 | 
|---|
| 1312 |                         dist += interval;
 | 
|---|
| 1313 |                     }
 | 
|---|
| 1314 |                 }
 | 
|---|
| 1315 |             });
 | 
|---|
| 1316 |         } else {
 | 
|---|
| 1317 |             onewayArrows = null;
 | 
|---|
| 1318 |             onewayArrowsCasing = null;
 | 
|---|
| 1319 |         }
 | 
|---|
| 1320 | 
 | 
|---|
| 1321 |         if (way.isHighlighted()) {
 | 
|---|
| 1322 |             drawPathHighlight(path, line);
 | 
|---|
| 1323 |         }
 | 
|---|
| 1324 |         displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor);
 | 
|---|
| 1325 |     }
 | 
|---|
| 1326 | 
 | 
|---|
| 1327 |     private static void appendOnewayPath(boolean onewayReversed, MapViewPoint p1, double nx, double ny, double dist,
 | 
|---|
| 1328 |             double onewaySize, Path2D onewayPath) {
 | 
|---|
| 1329 |         // scale such that border is 1 px
 | 
|---|
| 1330 |         final double fac = -(onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI);
 | 
|---|
| 1331 |         final double sx = nx * fac;
 | 
|---|
| 1332 |         final double sy = ny * fac;
 | 
|---|
| 1333 | 
 | 
|---|
| 1334 |         // Attach the triangle at the incenter and not at the tip.
 | 
|---|
| 1335 |         // Makes the border even at all sides.
 | 
|---|
| 1336 |         final double x = p1.getInViewX() + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
 | 
|---|
| 1337 |         final double y = p1.getInViewY() + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
 | 
|---|
| 1338 | 
 | 
|---|
| 1339 |         onewayPath.moveTo(x, y);
 | 
|---|
| 1340 |         onewayPath.lineTo(x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy);
 | 
|---|
| 1341 |         onewayPath.lineTo(x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy);
 | 
|---|
| 1342 |         onewayPath.lineTo(x, y);
 | 
|---|
| 1343 |     }
 | 
|---|
| 1344 | 
 | 
|---|
| 1345 |     /**
 | 
|---|
| 1346 |      * Gets the "circum". This is the distance on the map in meters that 100 screen pixels represent.
 | 
|---|
| 1347 |      * @return The "circum"
 | 
|---|
| 1348 |      */
 | 
|---|
| 1349 |     public double getCircum() {
 | 
|---|
| 1350 |         return circum;
 | 
|---|
| 1351 |     }
 | 
|---|
| 1352 | 
 | 
|---|
| 1353 |     @Override
 | 
|---|
| 1354 |     public void getColors() {
 | 
|---|
| 1355 |         super.getColors();
 | 
|---|
| 1356 |         this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100);
 | 
|---|
| 1357 |         this.backgroundColor = PaintColors.getBackgroundColor();
 | 
|---|
| 1358 |     }
 | 
|---|
| 1359 | 
 | 
|---|
| 1360 |     @Override
 | 
|---|
| 1361 |     public void getSettings(boolean virtual) {
 | 
|---|
| 1362 |         super.getSettings(virtual);
 | 
|---|
| 1363 |         paintSettings = MapPaintSettings.INSTANCE;
 | 
|---|
| 1364 | 
 | 
|---|
| 1365 |         circum = nc.getDist100Pixel();
 | 
|---|
| 1366 |         scale = nc.getScale();
 | 
|---|
| 1367 | 
 | 
|---|
| 1368 |         leftHandTraffic = PREFERENCE_LEFT_HAND_TRAFFIC.get();
 | 
|---|
| 1369 | 
 | 
|---|
| 1370 |         useStrokes = paintSettings.getUseStrokesDistance() > circum;
 | 
|---|
| 1371 |         showNames = paintSettings.getShowNamesDistance() > circum;
 | 
|---|
| 1372 |         showIcons = paintSettings.getShowIconsDistance() > circum;
 | 
|---|
| 1373 |         isOutlineOnly = paintSettings.isOutlineOnly();
 | 
|---|
| 1374 | 
 | 
|---|
| 1375 |         antialiasing = PREFERENCE_ANTIALIASING_USE.get() ?
 | 
|---|
| 1376 |                         RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF;
 | 
|---|
| 1377 |         g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
 | 
|---|
| 1378 | 
 | 
|---|
| 1379 |         Object textAntialiasing;
 | 
|---|
| 1380 |         switch (PREFERENCE_TEXT_ANTIALIASING.get()) {
 | 
|---|
| 1381 |             case "on":
 | 
|---|
| 1382 |                 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
 | 
|---|
| 1383 |                 break;
 | 
|---|
| 1384 |             case "off":
 | 
|---|
| 1385 |                 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
 | 
|---|
| 1386 |                 break;
 | 
|---|
| 1387 |             case "gasp":
 | 
|---|
| 1388 |                 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP;
 | 
|---|
| 1389 |                 break;
 | 
|---|
| 1390 |             case "lcd-hrgb":
 | 
|---|
| 1391 |                 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB;
 | 
|---|
| 1392 |                 break;
 | 
|---|
| 1393 |             case "lcd-hbgr":
 | 
|---|
| 1394 |                 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR;
 | 
|---|
| 1395 |                 break;
 | 
|---|
| 1396 |             case "lcd-vrgb":
 | 
|---|
| 1397 |                 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB;
 | 
|---|
| 1398 |                 break;
 | 
|---|
| 1399 |             case "lcd-vbgr":
 | 
|---|
| 1400 |                 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR;
 | 
|---|
| 1401 |                 break;
 | 
|---|
| 1402 |             default:
 | 
|---|
| 1403 |                 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT;
 | 
|---|
| 1404 |         }
 | 
|---|
| 1405 |         g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, textAntialiasing);
 | 
|---|
| 1406 |     }
 | 
|---|
| 1407 | 
 | 
|---|
| 1408 |     private MapViewPath getPath(Way w) {
 | 
|---|
| 1409 |         MapViewPath path = new MapViewPath(mapState);
 | 
|---|
| 1410 |         if (w.isClosed()) {
 | 
|---|
| 1411 |             path.appendClosed(w.getNodes(), false);
 | 
|---|
| 1412 |         } else {
 | 
|---|
| 1413 |             path.append(w.getNodes(), false);
 | 
|---|
| 1414 |         }
 | 
|---|
| 1415 |         return path;
 | 
|---|
| 1416 |     }
 | 
|---|
| 1417 | 
 | 
|---|
| 1418 |     private static Path2D.Double getPFClip(Way w, double extent) {
 | 
|---|
| 1419 |         Path2D.Double clip = new Path2D.Double();
 | 
|---|
| 1420 |         buildPFClip(clip, w.getNodes(), extent);
 | 
|---|
| 1421 |         return clip;
 | 
|---|
| 1422 |     }
 | 
|---|
| 1423 | 
 | 
|---|
| 1424 |     private static Path2D.Double getPFClip(PolyData pd, double extent) {
 | 
|---|
| 1425 |         Path2D.Double clip = new Path2D.Double();
 | 
|---|
| 1426 |         clip.setWindingRule(Path2D.WIND_EVEN_ODD);
 | 
|---|
| 1427 |         buildPFClip(clip, pd.getNodes(), extent);
 | 
|---|
| 1428 |         for (PolyData pdInner : pd.getInners()) {
 | 
|---|
| 1429 |             buildPFClip(clip, pdInner.getNodes(), extent);
 | 
|---|
| 1430 |         }
 | 
|---|
| 1431 |         return clip;
 | 
|---|
| 1432 |     }
 | 
|---|
| 1433 | 
 | 
|---|
| 1434 |     /**
 | 
|---|
| 1435 |      * Fix the clipping area of unclosed polygons for partial fill.
 | 
|---|
| 1436 |      *
 | 
|---|
| 1437 |      * The current algorithm for partial fill simply strokes the polygon with a
 | 
|---|
| 1438 |      * large stroke width after masking the outside with a clipping area.
 | 
|---|
| 1439 |      * This works, but for unclosed polygons, the mask can crop the corners at
 | 
|---|
| 1440 |      * both ends (see #12104).
 | 
|---|
| 1441 |      *
 | 
|---|
| 1442 |      * This method fixes the clipping area by sort of adding the corners to the
 | 
|---|
| 1443 |      * clip outline.
 | 
|---|
| 1444 |      *
 | 
|---|
| 1445 |      * @param clip the clipping area to modify (initially empty)
 | 
|---|
| 1446 |      * @param nodes nodes of the polygon
 | 
|---|
| 1447 |      * @param extent the extent
 | 
|---|
| 1448 |      */
 | 
|---|
| 1449 |     private static void buildPFClip(Path2D.Double clip, List<Node> nodes, double extent) {
 | 
|---|
| 1450 |         boolean initial = true;
 | 
|---|
| 1451 |         for (Node n : nodes) {
 | 
|---|
| 1452 |             EastNorth p = n.getEastNorth();
 | 
|---|
| 1453 |             if (p != null) {
 | 
|---|
| 1454 |                 if (initial) {
 | 
|---|
| 1455 |                     clip.moveTo(p.getX(), p.getY());
 | 
|---|
| 1456 |                     initial = false;
 | 
|---|
| 1457 |                 } else {
 | 
|---|
| 1458 |                     clip.lineTo(p.getX(), p.getY());
 | 
|---|
| 1459 |                 }
 | 
|---|
| 1460 |             }
 | 
|---|
| 1461 |         }
 | 
|---|
| 1462 |         if (nodes.size() >= 3) {
 | 
|---|
| 1463 |             EastNorth fst = nodes.get(0).getEastNorth();
 | 
|---|
| 1464 |             EastNorth snd = nodes.get(1).getEastNorth();
 | 
|---|
| 1465 |             EastNorth lst = nodes.get(nodes.size() - 1).getEastNorth();
 | 
|---|
| 1466 |             EastNorth lbo = nodes.get(nodes.size() - 2).getEastNorth();
 | 
|---|
| 1467 | 
 | 
|---|
| 1468 |             EastNorth cLst = getPFDisplacedEndPoint(lbo, lst, fst, extent);
 | 
|---|
| 1469 |             EastNorth cFst = getPFDisplacedEndPoint(snd, fst, cLst != null ? cLst : lst, extent);
 | 
|---|
| 1470 |             if (cLst == null && cFst != null) {
 | 
|---|
| 1471 |                 cLst = getPFDisplacedEndPoint(lbo, lst, cFst, extent);
 | 
|---|
| 1472 |             }
 | 
|---|
| 1473 |             if (cLst != null) {
 | 
|---|
| 1474 |                 clip.lineTo(cLst.getX(), cLst.getY());
 | 
|---|
| 1475 |             }
 | 
|---|
| 1476 |             if (cFst != null) {
 | 
|---|
| 1477 |                 clip.lineTo(cFst.getX(), cFst.getY());
 | 
|---|
| 1478 |             }
 | 
|---|
| 1479 |         }
 | 
|---|
| 1480 |     }
 | 
|---|
| 1481 | 
 | 
|---|
| 1482 |     /**
 | 
|---|
| 1483 |      * Get the point to add to the clipping area for partial fill of unclosed polygons.
 | 
|---|
| 1484 |      *
 | 
|---|
| 1485 |      * <code>(p1,p2)</code> is the first or last way segment and <code>p3</code> the
 | 
|---|
| 1486 |      * opposite endpoint.
 | 
|---|
| 1487 |      *
 | 
|---|
| 1488 |      * @param p1 1st point
 | 
|---|
| 1489 |      * @param p2 2nd point
 | 
|---|
| 1490 |      * @param p3 3rd point
 | 
|---|
| 1491 |      * @param extent the extent
 | 
|---|
| 1492 |      * @return a point q, such that p1,p2,q form a right angle
 | 
|---|
| 1493 |      * and the distance of q to p2 is <code>extent</code>. The point q lies on
 | 
|---|
| 1494 |      * the same side of the line p1,p2 as the point p3.
 | 
|---|
| 1495 |      * Returns null if p1,p2,p3 forms an angle greater 90 degrees. (In this case
 | 
|---|
| 1496 |      * the corner of the partial fill would not be cut off by the mask, so an
 | 
|---|
| 1497 |      * additional point is not necessary.)
 | 
|---|
| 1498 |      */
 | 
|---|
| 1499 |     private static EastNorth getPFDisplacedEndPoint(EastNorth p1, EastNorth p2, EastNorth p3, double extent) {
 | 
|---|
| 1500 |         double dx1 = p2.getX() - p1.getX();
 | 
|---|
| 1501 |         double dy1 = p2.getY() - p1.getY();
 | 
|---|
| 1502 |         double dx2 = p3.getX() - p2.getX();
 | 
|---|
| 1503 |         double dy2 = p3.getY() - p2.getY();
 | 
|---|
| 1504 |         if (dx1 * dx2 + dy1 * dy2 < 0) {
 | 
|---|
| 1505 |             double len = Math.sqrt(dx1 * dx1 + dy1 * dy1);
 | 
|---|
| 1506 |             if (len == 0) return null;
 | 
|---|
| 1507 |             double dxm = -dy1 * extent / len;
 | 
|---|
| 1508 |             double dym = dx1 * extent / len;
 | 
|---|
| 1509 |             if (dx1 * dy2 - dx2 * dy1 < 0) {
 | 
|---|
| 1510 |                 dxm = -dxm;
 | 
|---|
| 1511 |                 dym = -dym;
 | 
|---|
| 1512 |             }
 | 
|---|
| 1513 |             return new EastNorth(p2.getX() + dxm, p2.getY() + dym);
 | 
|---|
| 1514 |         }
 | 
|---|
| 1515 |         return null;
 | 
|---|
| 1516 |     }
 | 
|---|
| 1517 | 
 | 
|---|
| 1518 |     /**
 | 
|---|
| 1519 |      * Test if the area is visible
 | 
|---|
| 1520 |      * @param area The area, interpreted in east/north space.
 | 
|---|
| 1521 |      * @return true if it is visible.
 | 
|---|
| 1522 |      */
 | 
|---|
| 1523 |     private boolean isAreaVisible(Path2D.Double area) {
 | 
|---|
| 1524 |         Rectangle2D bounds = area.getBounds2D();
 | 
|---|
| 1525 |         if (bounds.isEmpty()) return false;
 | 
|---|
| 1526 |         MapViewPoint p = mapState.getPointFor(new EastNorth(bounds.getX(), bounds.getY()));
 | 
|---|
| 1527 |         if (p.getInViewY() < 0 || p.getInViewX() > mapState.getViewWidth()) return false;
 | 
|---|
| 1528 |         p = mapState.getPointFor(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()));
 | 
|---|
| 1529 |         return p.getInViewX() >= 0 && p.getInViewY() <= mapState.getViewHeight();
 | 
|---|
| 1530 |     }
 | 
|---|
| 1531 | 
 | 
|---|
| 1532 |     /**
 | 
|---|
| 1533 |      * Determines if the paint visitor shall render OSM objects such that they look inactive.
 | 
|---|
| 1534 |      * @return {@code true} if the paint visitor shall render OSM objects such that they look inactive
 | 
|---|
| 1535 |      */
 | 
|---|
| 1536 |     public boolean isInactiveMode() {
 | 
|---|
| 1537 |         return isInactiveMode;
 | 
|---|
| 1538 |     }
 | 
|---|
| 1539 | 
 | 
|---|
| 1540 |     /**
 | 
|---|
| 1541 |      * Check if icons should be rendered
 | 
|---|
| 1542 |      * @return <code>true</code> to display icons
 | 
|---|
| 1543 |      */
 | 
|---|
| 1544 |     public boolean isShowIcons() {
 | 
|---|
| 1545 |         return showIcons;
 | 
|---|
| 1546 |     }
 | 
|---|
| 1547 | 
 | 
|---|
| 1548 |     /**
 | 
|---|
| 1549 |      * Test if names should be rendered
 | 
|---|
| 1550 |      * @return <code>true</code> to display names
 | 
|---|
| 1551 |      */
 | 
|---|
| 1552 |     public boolean isShowNames() {
 | 
|---|
| 1553 |         return showNames;
 | 
|---|
| 1554 |     }
 | 
|---|
| 1555 | 
 | 
|---|
| 1556 |     /**
 | 
|---|
| 1557 |      * Computes the flags for a given OSM primitive.
 | 
|---|
| 1558 |      * @param primitive The primititve to compute the flags for.
 | 
|---|
| 1559 |      * @param checkOuterMember <code>true</code> if we should also add {@link #FLAG_OUTERMEMBER_OF_SELECTED}
 | 
|---|
| 1560 |      * @return The flag.
 | 
|---|
| 1561 |      */
 | 
|---|
| 1562 |     public static int computeFlags(OsmPrimitive primitive, boolean checkOuterMember) {
 | 
|---|
| 1563 |         if (primitive.isDisabled()) {
 | 
|---|
| 1564 |             return FLAG_DISABLED;
 | 
|---|
| 1565 |         } else if (primitive.isSelected()) {
 | 
|---|
| 1566 |             return FLAG_SELECTED;
 | 
|---|
| 1567 |         } else if (checkOuterMember && primitive.isOuterMemberOfSelected()) {
 | 
|---|
| 1568 |             return FLAG_OUTERMEMBER_OF_SELECTED;
 | 
|---|
| 1569 |         } else if (primitive.isMemberOfSelected()) {
 | 
|---|
| 1570 |             return FLAG_MEMBER_OF_SELECTED;
 | 
|---|
| 1571 |         } else {
 | 
|---|
| 1572 |             return FLAG_NORMAL;
 | 
|---|
| 1573 |         }
 | 
|---|
| 1574 |     }
 | 
|---|
| 1575 | 
 | 
|---|
| 1576 |     /**
 | 
|---|
| 1577 |      * Sets the factory that creates the benchmark data receivers.
 | 
|---|
| 1578 |      * @param benchmarkFactory The factory.
 | 
|---|
| 1579 |      * @since 10697
 | 
|---|
| 1580 |      */
 | 
|---|
| 1581 |     public void setBenchmarkFactory(Supplier<RenderBenchmarkCollector> benchmarkFactory) {
 | 
|---|
| 1582 |         this.benchmarkFactory = benchmarkFactory;
 | 
|---|
| 1583 |     }
 | 
|---|
| 1584 | 
 | 
|---|
| 1585 |     @Override
 | 
|---|
| 1586 |     public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) {
 | 
|---|
| 1587 |         RenderBenchmarkCollector benchmark = benchmarkFactory.get();
 | 
|---|
| 1588 |         BBox bbox = bounds.toBBox();
 | 
|---|
| 1589 |         getSettings(renderVirtualNodes);
 | 
|---|
| 1590 | 
 | 
|---|
| 1591 |         try {
 | 
|---|
| 1592 |             if (data.getReadLock().tryLock(1, TimeUnit.SECONDS)) {
 | 
|---|
| 1593 |                 try {
 | 
|---|
| 1594 |                     paintWithLock(data, renderVirtualNodes, benchmark, bbox);
 | 
|---|
| 1595 |                 } finally {
 | 
|---|
| 1596 |                     data.getReadLock().unlock();
 | 
|---|
| 1597 |                 }
 | 
|---|
| 1598 |             } else {
 | 
|---|
| 1599 |                 Logging.warn("Cannot paint layer {0}: It is locked.");
 | 
|---|
| 1600 |             }
 | 
|---|
| 1601 |         } catch (InterruptedException e) {
 | 
|---|
| 1602 |             Logging.warn("Cannot paint layer {0}: Interrupted");
 | 
|---|
| 1603 |         }
 | 
|---|
| 1604 |     }
 | 
|---|
| 1605 | 
 | 
|---|
| 1606 |     private void paintWithLock(final DataSet data, boolean renderVirtualNodes, RenderBenchmarkCollector benchmark,
 | 
|---|
| 1607 |             BBox bbox) {
 | 
|---|
| 1608 |         try {
 | 
|---|
| 1609 |             highlightWaySegments = data.getHighlightedWaySegments();
 | 
|---|
| 1610 | 
 | 
|---|
| 1611 |             benchmark.renderStart(circum);
 | 
|---|
| 1612 | 
 | 
|---|
| 1613 |             List<Node> nodes = data.searchNodes(bbox);
 | 
|---|
| 1614 |             List<Way> ways = data.searchWays(bbox);
 | 
|---|
| 1615 |             List<Relation> relations = data.searchRelations(bbox);
 | 
|---|
| 1616 | 
 | 
|---|
| 1617 |             final List<StyleRecord> allStyleElems = new ArrayList<>(nodes.size()+ways.size()+relations.size());
 | 
|---|
| 1618 | 
 | 
|---|
| 1619 |             // Need to process all relations first.
 | 
|---|
| 1620 |             // Reason: Make sure, ElemStyles.getStyleCacheWithRange is not called for the same primitive in parallel threads.
 | 
|---|
| 1621 |             // (Could be synchronized, but try to avoid this for performance reasons.)
 | 
|---|
| 1622 |             THREAD_POOL.invoke(new ComputeStyleListWorker(circum, nc, relations, allStyleElems,
 | 
|---|
| 1623 |                     Math.max(20, relations.size() / THREAD_POOL.getParallelism() / 3)));
 | 
|---|
| 1624 |             THREAD_POOL.invoke(new ComputeStyleListWorker(circum, nc, new CompositeList<>(nodes, ways), allStyleElems,
 | 
|---|
| 1625 |                     Math.max(100, (nodes.size() + ways.size()) / THREAD_POOL.getParallelism() / 3)));
 | 
|---|
| 1626 | 
 | 
|---|
| 1627 |             if (!benchmark.renderSort()) {
 | 
|---|
| 1628 |                 return;
 | 
|---|
| 1629 |             }
 | 
|---|
| 1630 | 
 | 
|---|
| 1631 |             // We use parallel sort here. This is only available for arrays.
 | 
|---|
| 1632 |             StyleRecord[] sorted = allStyleElems.toArray(new StyleRecord[allStyleElems.size()]);
 | 
|---|
| 1633 |             Arrays.parallelSort(sorted, null);
 | 
|---|
| 1634 | 
 | 
|---|
| 1635 |             if (!benchmark.renderDraw(allStyleElems)) {
 | 
|---|
| 1636 |                 return;
 | 
|---|
| 1637 |             }
 | 
|---|
| 1638 | 
 | 
|---|
| 1639 |             for (StyleRecord record : sorted) {
 | 
|---|
| 1640 |                 paintRecord(record);
 | 
|---|
| 1641 |             }
 | 
|---|
| 1642 | 
 | 
|---|
| 1643 |             drawVirtualNodes(data, bbox);
 | 
|---|
| 1644 | 
 | 
|---|
| 1645 |             benchmark.renderDone();
 | 
|---|
| 1646 |         } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
 | 
|---|
| 1647 |             throw BugReport.intercept(e)
 | 
|---|
| 1648 |                     .put("data", data)
 | 
|---|
| 1649 |                     .put("circum", circum)
 | 
|---|
| 1650 |                     .put("scale", scale)
 | 
|---|
| 1651 |                     .put("paintSettings", paintSettings)
 | 
|---|
| 1652 |                     .put("renderVirtualNodes", renderVirtualNodes);
 | 
|---|
| 1653 |         }
 | 
|---|
| 1654 |     }
 | 
|---|
| 1655 | 
 | 
|---|
| 1656 |     private void paintRecord(StyleRecord record) {
 | 
|---|
| 1657 |         try {
 | 
|---|
| 1658 |             record.paintPrimitive(paintSettings, this);
 | 
|---|
| 1659 |         } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
 | 
|---|
| 1660 |             throw BugReport.intercept(e).put("record", record);
 | 
|---|
| 1661 |         }
 | 
|---|
| 1662 |     }
 | 
|---|
| 1663 | }
 | 
|---|