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}