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