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