source: josm/trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java@ 9949

Last change on this file since 9949 was 9949, checked in by Don-vip, 8 years ago

remove deprecated stuff, code cleanup, javadoc, unit tests

  • Property svn:eol-style set to native
File size: 15.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trc;
7
8import java.awt.Color;
9import java.awt.Component;
10import java.awt.Font;
11import java.awt.Graphics2D;
12import java.awt.GridBagLayout;
13import java.awt.Transparency;
14import java.awt.event.ActionEvent;
15import java.awt.font.FontRenderContext;
16import java.awt.font.LineBreakMeasurer;
17import java.awt.font.TextAttribute;
18import java.awt.font.TextLayout;
19import java.awt.image.BufferedImage;
20import java.awt.image.BufferedImageOp;
21import java.awt.image.ConvolveOp;
22import java.awt.image.Kernel;
23import java.awt.image.LookupOp;
24import java.awt.image.ShortLookupTable;
25import java.text.AttributedCharacterIterator;
26import java.text.AttributedString;
27import java.util.ArrayList;
28import java.util.HashMap;
29import java.util.List;
30import java.util.Map;
31
32import javax.swing.AbstractAction;
33import javax.swing.Icon;
34import javax.swing.JCheckBoxMenuItem;
35import javax.swing.JComponent;
36import javax.swing.JLabel;
37import javax.swing.JMenu;
38import javax.swing.JMenuItem;
39import javax.swing.JPanel;
40import javax.swing.JPopupMenu;
41import javax.swing.JSeparator;
42
43import org.openstreetmap.josm.Main;
44import org.openstreetmap.josm.actions.ImageryAdjustAction;
45import org.openstreetmap.josm.data.ProjectionBounds;
46import org.openstreetmap.josm.data.imagery.ImageryInfo;
47import org.openstreetmap.josm.data.imagery.OffsetBookmark;
48import org.openstreetmap.josm.data.preferences.ColorProperty;
49import org.openstreetmap.josm.data.preferences.IntegerProperty;
50import org.openstreetmap.josm.gui.MenuScroller;
51import org.openstreetmap.josm.gui.widgets.UrlLabel;
52import org.openstreetmap.josm.tools.GBC;
53import org.openstreetmap.josm.tools.ImageProvider;
54import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
55import org.openstreetmap.josm.tools.Utils;
56
57public abstract class ImageryLayer extends Layer {
58
59 public static final ColorProperty PROP_FADE_COLOR = new ColorProperty(marktr("Imagery fade"), Color.white);
60 public static final IntegerProperty PROP_FADE_AMOUNT = new IntegerProperty("imagery.fade_amount", 0);
61 public static final IntegerProperty PROP_SHARPEN_LEVEL = new IntegerProperty("imagery.sharpen_level", 0);
62
63 private final List<ImageProcessor> imageProcessors = new ArrayList<>();
64
65 public static Color getFadeColor() {
66 return PROP_FADE_COLOR.get();
67 }
68
69 public static Color getFadeColorWithAlpha() {
70 Color c = PROP_FADE_COLOR.get();
71 return new Color(c.getRed(), c.getGreen(), c.getBlue(), PROP_FADE_AMOUNT.get()*255/100);
72 }
73
74 protected final ImageryInfo info;
75
76 protected Icon icon;
77
78 protected double dx;
79 protected double dy;
80
81 protected GammaImageProcessor gammaImageProcessor = new GammaImageProcessor();
82
83 private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this);
84
85 /**
86 * Constructs a new {@code ImageryLayer}.
87 * @param info imagery info
88 */
89 public ImageryLayer(ImageryInfo info) {
90 super(info.getName());
91 this.info = info;
92 if (info.getIcon() != null) {
93 icon = new ImageProvider(info.getIcon()).setOptional(true).
94 setMaxSize(ImageSizes.LAYER).get();
95 }
96 if (icon == null) {
97 icon = ImageProvider.get("imagery_small");
98 }
99 addImageProcessor(createSharpener(PROP_SHARPEN_LEVEL.get()));
100 addImageProcessor(gammaImageProcessor);
101 }
102
103 public double getPPD() {
104 if (!Main.isDisplayingMapView()) return Main.getProjection().getDefaultZoomInPPD();
105 ProjectionBounds bounds = Main.map.mapView.getProjectionBounds();
106 return Main.map.mapView.getWidth() / (bounds.maxEast - bounds.minEast);
107 }
108
109 public double getDx() {
110 return dx;
111 }
112
113 public double getDy() {
114 return dy;
115 }
116
117 public void setOffset(double dx, double dy) {
118 this.dx = dx;
119 this.dy = dy;
120 }
121
122 public void displace(double dx, double dy) {
123 setOffset(this.dx += dx, this.dy += dy);
124 }
125
126 public ImageryInfo getInfo() {
127 return info;
128 }
129
130 @Override
131 public Icon getIcon() {
132 return icon;
133 }
134
135 @Override
136 public boolean isMergable(Layer other) {
137 return false;
138 }
139
140 @Override
141 public void mergeFrom(Layer from) {
142 }
143
144 @Override
145 public Object getInfoComponent() {
146 JPanel panel = new JPanel(new GridBagLayout());
147 panel.add(new JLabel(getToolTipText()), GBC.eol());
148 if (info != null) {
149 String url = info.getUrl();
150 if (url != null) {
151 panel.add(new JLabel(tr("URL: ")), GBC.std().insets(0, 5, 2, 0));
152 panel.add(new UrlLabel(url), GBC.eol().insets(2, 5, 10, 0));
153 }
154 if (dx != 0 || dy != 0) {
155 panel.add(new JLabel(tr("Offset: ") + dx + ';' + dy), GBC.eol().insets(0, 5, 10, 0));
156 }
157 }
158 return panel;
159 }
160
161 public static ImageryLayer create(ImageryInfo info) {
162 switch(info.getImageryType()) {
163 case WMS:
164 case HTML:
165 return new WMSLayer(info);
166 case WMTS:
167 return new WMTSLayer(info);
168 case TMS:
169 case BING:
170 case SCANEX:
171 return new TMSLayer(info);
172 default:
173 throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType()));
174 }
175 }
176
177 class ApplyOffsetAction extends AbstractAction {
178 private final transient OffsetBookmark b;
179
180 ApplyOffsetAction(OffsetBookmark b) {
181 super(b.name);
182 this.b = b;
183 }
184
185 @Override
186 public void actionPerformed(ActionEvent ev) {
187 setOffset(b.dx, b.dy);
188 Main.main.menu.imageryMenu.refreshOffsetMenu();
189 Main.map.repaint();
190 }
191 }
192
193 public class OffsetAction extends AbstractAction implements LayerAction {
194 @Override
195 public void actionPerformed(ActionEvent e) {
196 }
197
198 @Override
199 public Component createMenuComponent() {
200 return getOffsetMenuItem();
201 }
202
203 @Override
204 public boolean supportLayers(List<Layer> layers) {
205 return false;
206 }
207 }
208
209 public JMenuItem getOffsetMenuItem() {
210 JMenu subMenu = new JMenu(trc("layer", "Offset"));
211 subMenu.setIcon(ImageProvider.get("mapmode", "adjustimg"));
212 return (JMenuItem) getOffsetMenuItem(subMenu);
213 }
214
215 public JComponent getOffsetMenuItem(JComponent subMenu) {
216 JMenuItem adjustMenuItem = new JMenuItem(adjustAction);
217 if (OffsetBookmark.allBookmarks.isEmpty()) return adjustMenuItem;
218
219 subMenu.add(adjustMenuItem);
220 subMenu.add(new JSeparator());
221 boolean hasBookmarks = false;
222 int menuItemHeight = 0;
223 for (OffsetBookmark b : OffsetBookmark.allBookmarks) {
224 if (!b.isUsable(this)) {
225 continue;
226 }
227 JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b));
228 if (Utils.equalsEpsilon(b.dx, dx) && Utils.equalsEpsilon(b.dy, dy)) {
229 item.setSelected(true);
230 }
231 subMenu.add(item);
232 menuItemHeight = item.getPreferredSize().height;
233 hasBookmarks = true;
234 }
235 if (menuItemHeight > 0) {
236 if (subMenu instanceof JMenu) {
237 MenuScroller.setScrollerFor((JMenu) subMenu);
238 } else if (subMenu instanceof JPopupMenu) {
239 MenuScroller.setScrollerFor((JPopupMenu) subMenu);
240 }
241 }
242 return hasBookmarks ? subMenu : adjustMenuItem;
243 }
244
245 public ImageProcessor createSharpener(int sharpenLevel) {
246 final Kernel kernel;
247 if (sharpenLevel == 1) {
248 kernel = new Kernel(3, 3, new float[]{-0.25f, -0.5f, -0.25f, -0.5f, 4, -0.5f, -0.25f, -0.5f, -0.25f});
249 } else if (sharpenLevel == 2) {
250 kernel = new Kernel(3, 3, new float[]{-0.5f, -1, -0.5f, -1, 7, -1, -0.5f, -1, -0.5f});
251 } else {
252 return null;
253 }
254 BufferedImageOp op = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
255 return createImageProcessor(op, false);
256 }
257
258 /**
259 * An image processor which adjusts the gamma value of an image.
260 */
261 public static class GammaImageProcessor implements ImageProcessor {
262 private double gamma = 1;
263 final short[] gammaChange = new short[256];
264 private final LookupOp op3 = new LookupOp(
265 new ShortLookupTable(0, new short[][]{gammaChange, gammaChange, gammaChange}), null);
266 private final LookupOp op4 = new LookupOp(
267 new ShortLookupTable(0, new short[][]{gammaChange, gammaChange, gammaChange, gammaChange}), null);
268
269 /**
270 * Returns the currently set gamma value.
271 * @return the currently set gamma value
272 */
273 public double getGamma() {
274 return gamma;
275 }
276
277 /**
278 * Sets a new gamma value, {@code 1} stands for no correction.
279 * @param gamma new gamma value
280 */
281 public void setGamma(double gamma) {
282 this.gamma = gamma;
283 for (int i = 0; i < 256; i++) {
284 gammaChange[i] = (short) (255 * Math.pow(i / 255., gamma));
285 }
286 }
287
288 @Override
289 public BufferedImage process(BufferedImage image) {
290 if (gamma == 1) {
291 return image;
292 }
293 try {
294 final int bands = image.getRaster().getNumBands();
295 if (image.getType() != BufferedImage.TYPE_CUSTOM && bands == 3) {
296 return op3.filter(image, null);
297 } else if (image.getType() != BufferedImage.TYPE_CUSTOM && bands == 4) {
298 return op4.filter(image, null);
299 }
300 } catch (IllegalArgumentException ignore) {
301 if (Main.isTraceEnabled()) {
302 Main.trace(ignore.getMessage());
303 }
304 }
305 final int type = image.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
306 final BufferedImage to = new BufferedImage(image.getWidth(), image.getHeight(), type);
307 to.getGraphics().drawImage(image, 0, 0, null);
308 return process(to);
309 }
310 }
311
312 /**
313 * Returns the currently set gamma value.
314 * @return the currently set gamma value
315 */
316 public double getGamma() {
317 return gammaImageProcessor.getGamma();
318 }
319
320 /**
321 * Sets a new gamma value, {@code 1} stands for no correction.
322 * @param gamma new gamma value
323 */
324 public void setGamma(double gamma) {
325 gammaImageProcessor.setGamma(gamma);
326 }
327
328 /**
329 * This method adds the {@link ImageProcessor} to this Layer if it is not {@code null}.
330 *
331 * @param processor that processes the image
332 *
333 * @return true if processor was added, false otherwise
334 */
335 public boolean addImageProcessor(ImageProcessor processor) {
336 return processor != null && imageProcessors.add(processor);
337 }
338
339 /**
340 * This method removes given {@link ImageProcessor} from this layer
341 *
342 * @param processor which is needed to be removed
343 *
344 * @return true if processor was removed
345 */
346 public boolean removeImageProcessor(ImageProcessor processor) {
347 return imageProcessors.remove(processor);
348 }
349
350 /**
351 * Wraps a {@link BufferedImageOp} to be used as {@link ImageProcessor}.
352 * @param op the {@link BufferedImageOp}
353 * @param inPlace true to apply filter in place, i.e., not create a new {@link BufferedImage} for the result
354 * (the {@code op} needs to support this!)
355 * @return the {@link ImageProcessor} wrapper
356 */
357 public static ImageProcessor createImageProcessor(final BufferedImageOp op, final boolean inPlace) {
358 return new ImageProcessor() {
359 @Override
360 public BufferedImage process(BufferedImage image) {
361 return op.filter(image, inPlace ? image : null);
362 }
363 };
364 }
365
366 /**
367 * This method gets all {@link ImageProcessor}s of the layer
368 *
369 * @return list of image processors without removed one
370 */
371 public List<ImageProcessor> getImageProcessors() {
372 return imageProcessors;
373 }
374
375 /**
376 * Applies all the chosen {@link ImageProcessor}s to the image
377 *
378 * @param img - image which should be changed
379 *
380 * @return the new changed image
381 */
382 public BufferedImage applyImageProcessors(BufferedImage img) {
383 for (ImageProcessor processor : imageProcessors) {
384 img = processor.process(img);
385 }
386 return img;
387 }
388
389 /**
390 * Draws a red error tile when imagery tile cannot be fetched.
391 * @param img The buffered image
392 * @param message Additional error message to display
393 */
394 public void drawErrorTile(BufferedImage img, String message) {
395 Graphics2D g = (Graphics2D) img.getGraphics();
396 g.setColor(Color.RED);
397 g.fillRect(0, 0, img.getWidth(), img.getHeight());
398 g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(24.0f));
399 g.setColor(Color.BLACK);
400
401 String text = tr("ERROR");
402 g.drawString(text, (img.getWidth() - g.getFontMetrics().stringWidth(text)) / 2, g.getFontMetrics().getHeight()+5);
403 if (message != null) {
404 float drawPosY = 2.5f*g.getFontMetrics().getHeight()+10;
405 if (!message.contains(" ")) {
406 g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(18.0f));
407 g.drawString(message, 5, (int) drawPosY);
408 } else {
409 // Draw message on several lines
410 Map<TextAttribute, Object> map = new HashMap<>();
411 map.put(TextAttribute.FAMILY, "Serif");
412 map.put(TextAttribute.SIZE, new Float(18.0));
413 AttributedString vanGogh = new AttributedString(message, map);
414 // Create a new LineBreakMeasurer from the text
415 AttributedCharacterIterator paragraph = vanGogh.getIterator();
416 int paragraphStart = paragraph.getBeginIndex();
417 int paragraphEnd = paragraph.getEndIndex();
418 FontRenderContext frc = g.getFontRenderContext();
419 LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
420 // Set break width to width of image with some margin
421 float breakWidth = img.getWidth()-10;
422 // Set position to the index of the first character in the text
423 lineMeasurer.setPosition(paragraphStart);
424 // Get lines until the entire paragraph has been displayed
425 while (lineMeasurer.getPosition() < paragraphEnd) {
426 // Retrieve next layout
427 TextLayout layout = lineMeasurer.nextLayout(breakWidth);
428
429 // Compute pen x position
430 float drawPosX = layout.isLeftToRight() ? 0 : breakWidth - layout.getAdvance();
431
432 // Move y-coordinate by the ascent of the layout
433 drawPosY += layout.getAscent();
434
435 // Draw the TextLayout at (drawPosX, drawPosY)
436 layout.draw(g, drawPosX, drawPosY);
437
438 // Move y-coordinate in preparation for next layout
439 drawPosY += layout.getDescent() + layout.getLeading();
440 }
441 }
442 }
443 }
444
445 @Override
446 public void destroy() {
447 super.destroy();
448 adjustAction.destroy();
449 }
450}
Note: See TracBrowser for help on using the repository browser.