001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.marktr; 006 007import java.awt.Color; 008import java.awt.Dimension; 009import java.awt.Graphics; 010import java.awt.geom.Rectangle2D; 011import java.util.function.DoubleSupplier; 012import java.util.function.Supplier; 013 014import javax.accessibility.Accessible; 015import javax.accessibility.AccessibleContext; 016import javax.accessibility.AccessibleValue; 017import javax.swing.JComponent; 018 019import org.openstreetmap.josm.data.preferences.NamedColorProperty; 020import org.openstreetmap.josm.gui.help.Helpful; 021 022/** 023 * Map scale bar, displaying the distance in meter that correspond to 100 px on screen. 024 * @since 115 025 */ 026public class MapScaler extends JComponent implements Helpful, Accessible { 027 028 private final DoubleSupplier getDist100Pixel; 029 private final Supplier<Color> colorSupplier; 030 031 private static final int PADDING_LEFT = 5; 032 private static final int PADDING_RIGHT = 50; 033 034 private static final NamedColorProperty SCALER_COLOR = new NamedColorProperty(marktr("scale"), Color.WHITE); 035 036 /** 037 * Constructs a new {@code MapScaler}. 038 * @param mv map view 039 */ 040 public MapScaler(NavigatableComponent mv) { 041 this(() -> mv.getDist100Pixel(true), MapScaler::getColor); 042 } 043 044 /** 045 * Constructs a new {@code MapScaler}. 046 * @param getDist100Pixel supplier for distance in meter that correspond to 100 px on screen 047 * @param colorSupplier supplier for color 048 */ 049 public MapScaler(DoubleSupplier getDist100Pixel, Supplier<Color> colorSupplier) { 050 this.getDist100Pixel = getDist100Pixel; 051 this.colorSupplier = colorSupplier; 052 setPreferredLineLength(100); 053 setOpaque(false); 054 } 055 056 /** 057 * Sets the preferred length the distance line should have. 058 * @param pixel The length. 059 */ 060 public void setPreferredLineLength(int pixel) { 061 setPreferredSize(new Dimension(pixel + PADDING_LEFT + PADDING_RIGHT, 30)); 062 } 063 064 @Override 065 public void paint(Graphics g) { 066 g.setColor(colorSupplier.get()); 067 double dist100Pixel = getDist100Pixel.getAsDouble(); 068 TickMarks tickMarks = new TickMarks(dist100Pixel, getWidth() - PADDING_LEFT - PADDING_RIGHT); 069 tickMarks.paintTicks(g); 070 } 071 072 /** 073 * Returns the color of map scaler. 074 * @return the color of map scaler 075 */ 076 public static Color getColor() { 077 return SCALER_COLOR.get(); 078 } 079 080 @Override 081 public String helpTopic() { 082 return ht("/MapView/Scaler"); 083 } 084 085 @Override 086 public AccessibleContext getAccessibleContext() { 087 if (accessibleContext == null) { 088 accessibleContext = new AccessibleMapScaler(); 089 } 090 return accessibleContext; 091 } 092 093 class AccessibleMapScaler extends AccessibleJComponent implements AccessibleValue { 094 095 @Override 096 public Number getCurrentAccessibleValue() { 097 return getDist100Pixel.getAsDouble(); 098 } 099 100 @Override 101 public boolean setCurrentAccessibleValue(Number n) { 102 return false; 103 } 104 105 @Override 106 public Number getMinimumAccessibleValue() { 107 return null; 108 } 109 110 @Override 111 public Number getMaximumAccessibleValue() { 112 return null; 113 } 114 } 115 116 /** 117 * This class finds the best possible tick mark positions. 118 * <p> 119 * It will attempt to use steps of 1m, 2.5m, 10m, 25m, ... 120 */ 121 private static final class TickMarks { 122 123 private final double dist100Pixel; 124 /** 125 * Distance in meters between two ticks. 126 */ 127 private final double spacingMeter; 128 private final int steps; 129 private final int minorStepsPerMajor; 130 131 /** 132 * Creates a new tick mark helper. 133 * @param dist100Pixel The distance of 100 pixel on the map. 134 * @param width The width of the mark. 135 */ 136 TickMarks(double dist100Pixel, int width) { 137 this.dist100Pixel = dist100Pixel; 138 double lineDistance = dist100Pixel * width / 100; 139 140 double log10 = Math.log(lineDistance) / Math.log(10); 141 double spacingLog10 = Math.pow(10, Math.floor(log10)); 142 int minorStepsPerMajor; 143 double distanceBetweenMinor; 144 if (log10 - Math.floor(log10) < .75) { 145 // Add 2 ticks for every full unit 146 distanceBetweenMinor = spacingLog10 / 2; 147 minorStepsPerMajor = 2; 148 } else { 149 // Add 10 ticks for every full unit 150 distanceBetweenMinor = spacingLog10; 151 minorStepsPerMajor = 5; 152 } 153 // round down to the last major step. 154 int majorSteps = (int) Math.floor(lineDistance / distanceBetweenMinor / minorStepsPerMajor); 155 if (majorSteps >= 4) { 156 // we have many major steps, do not paint the minor now. 157 this.spacingMeter = distanceBetweenMinor * minorStepsPerMajor; 158 this.minorStepsPerMajor = 1; 159 } else { 160 this.minorStepsPerMajor = minorStepsPerMajor; 161 this.spacingMeter = distanceBetweenMinor; 162 } 163 steps = majorSteps * this.minorStepsPerMajor; 164 } 165 166 /** 167 * Paint the ticks to the graphics. 168 * @param g The graphics to paint on. 169 */ 170 public void paintTicks(Graphics g) { 171 double spacingPixel = spacingMeter / (dist100Pixel / 100); 172 double textBlockedUntil = -1; 173 for (int step = 0; step <= steps; step++) { 174 int x = (int) (PADDING_LEFT + spacingPixel * step); 175 boolean isMajor = step % minorStepsPerMajor == 0; 176 int paddingY = isMajor ? 0 : 3; 177 g.drawLine(x, paddingY, x, 10 - paddingY); 178 179 if (step == 0 || step == steps) { 180 String text; 181 if (step == 0) { 182 text = "0"; 183 } else { 184 text = NavigatableComponent.getDistText(spacingMeter * step); 185 } 186 Rectangle2D bound = g.getFontMetrics().getStringBounds(text, g); 187 int left = (int) (x - bound.getWidth() / 2); 188 if (textBlockedUntil > left) { 189 left = (int) (textBlockedUntil + 5); 190 } 191 g.drawString(text, left, 23); 192 textBlockedUntil = left + bound.getWidth() + 2; 193 } 194 } 195 g.drawLine(PADDING_LEFT + 0, 5, (int) (PADDING_LEFT + spacingPixel * steps), 5); 196 } 197 } 198}