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}