1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.gui;
|
---|
3 |
|
---|
4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
|
---|
5 | import static org.openstreetmap.josm.tools.I18n.marktr;
|
---|
6 |
|
---|
7 | import java.awt.Color;
|
---|
8 | import java.awt.Dimension;
|
---|
9 | import java.awt.Graphics;
|
---|
10 | import java.awt.geom.Rectangle2D;
|
---|
11 | import java.util.function.DoubleSupplier;
|
---|
12 | import java.util.function.Supplier;
|
---|
13 |
|
---|
14 | import javax.accessibility.Accessible;
|
---|
15 | import javax.accessibility.AccessibleContext;
|
---|
16 | import javax.accessibility.AccessibleValue;
|
---|
17 | import javax.swing.JComponent;
|
---|
18 |
|
---|
19 | import org.openstreetmap.josm.data.preferences.NamedColorProperty;
|
---|
20 | import org.openstreetmap.josm.gui.help.Helpful;
|
---|
21 |
|
---|
22 | /**
|
---|
23 | * Map scale bar, displaying the distance in meter that correspond to 100 px on screen.
|
---|
24 | * @since 115
|
---|
25 | */
|
---|
26 | public class MapScaler extends JComponent implements Helpful, Accessible {
|
---|
27 |
|
---|
28 | private final DoubleSupplier getDist100Pixel;
|
---|
29 | private final Supplier<Color> colorSupplier;
|
---|
30 |
|
---|
31 | private static final int PADDING_LEFT = 5;
|
---|
32 | private static final int PADDING_RIGHT = 50;
|
---|
33 |
|
---|
34 | private static final NamedColorProperty SCALER_COLOR = new NamedColorProperty(marktr("scale"), Color.WHITE);
|
---|
35 |
|
---|
36 | /**
|
---|
37 | * Constructs a new {@code MapScaler}.
|
---|
38 | * @param mv map view
|
---|
39 | */
|
---|
40 | public MapScaler(NavigatableComponent mv) {
|
---|
41 | this(() -> mv.getDist100Pixel(true), MapScaler::getColor);
|
---|
42 | }
|
---|
43 |
|
---|
44 | /**
|
---|
45 | * Constructs a new {@code MapScaler}.
|
---|
46 | * @param getDist100Pixel supplier for distance in meter that correspond to 100 px on screen
|
---|
47 | * @param colorSupplier supplier for color
|
---|
48 | */
|
---|
49 | public MapScaler(DoubleSupplier getDist100Pixel, Supplier<Color> colorSupplier) {
|
---|
50 | this.getDist100Pixel = getDist100Pixel;
|
---|
51 | this.colorSupplier = colorSupplier;
|
---|
52 | setPreferredLineLength(100);
|
---|
53 | setOpaque(false);
|
---|
54 | }
|
---|
55 |
|
---|
56 | /**
|
---|
57 | * Sets the preferred length the distance line should have.
|
---|
58 | * @param pixel The length.
|
---|
59 | */
|
---|
60 | public void setPreferredLineLength(int pixel) {
|
---|
61 | setPreferredSize(new Dimension(pixel + PADDING_LEFT + PADDING_RIGHT, 30));
|
---|
62 | }
|
---|
63 |
|
---|
64 | @Override
|
---|
65 | public void paint(Graphics g) {
|
---|
66 | g.setColor(colorSupplier.get());
|
---|
67 | double dist100Pixel = getDist100Pixel.getAsDouble();
|
---|
68 | TickMarks tickMarks = new TickMarks(dist100Pixel, getWidth() - PADDING_LEFT - PADDING_RIGHT);
|
---|
69 | tickMarks.paintTicks(g);
|
---|
70 | }
|
---|
71 |
|
---|
72 | /**
|
---|
73 | * Returns the color of map scaler.
|
---|
74 | * @return the color of map scaler
|
---|
75 | */
|
---|
76 | public static Color getColor() {
|
---|
77 | return SCALER_COLOR.get();
|
---|
78 | }
|
---|
79 |
|
---|
80 | @Override
|
---|
81 | public String helpTopic() {
|
---|
82 | return ht("/MapView/Scaler");
|
---|
83 | }
|
---|
84 |
|
---|
85 | @Override
|
---|
86 | public AccessibleContext getAccessibleContext() {
|
---|
87 | if (accessibleContext == null) {
|
---|
88 | accessibleContext = new AccessibleMapScaler();
|
---|
89 | }
|
---|
90 | return accessibleContext;
|
---|
91 | }
|
---|
92 |
|
---|
93 | class AccessibleMapScaler extends AccessibleJComponent implements AccessibleValue {
|
---|
94 |
|
---|
95 | @Override
|
---|
96 | public Number getCurrentAccessibleValue() {
|
---|
97 | return getDist100Pixel.getAsDouble();
|
---|
98 | }
|
---|
99 |
|
---|
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 | }
|
---|