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

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

sonar - squid:S1166 - Exception handlers should preserve the original exceptions

  • Property svn:eol-style set to native
File size: 22.1 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.GridBagLayout;
11import java.awt.Rectangle;
12import java.awt.RenderingHints;
13import java.awt.Transparency;
14import java.awt.event.ActionEvent;
15import java.awt.geom.Point2D;
16import java.awt.geom.Rectangle2D;
17import java.awt.image.BufferedImage;
18import java.awt.image.BufferedImageOp;
19import java.awt.image.ColorModel;
20import java.awt.image.ConvolveOp;
21import java.awt.image.DataBuffer;
22import java.awt.image.DataBufferByte;
23import java.awt.image.Kernel;
24import java.awt.image.LookupOp;
25import java.awt.image.ShortLookupTable;
26import java.util.ArrayList;
27import java.util.List;
28
29import javax.swing.AbstractAction;
30import javax.swing.Icon;
31import javax.swing.JCheckBoxMenuItem;
32import javax.swing.JComponent;
33import javax.swing.JLabel;
34import javax.swing.JMenu;
35import javax.swing.JMenuItem;
36import javax.swing.JPanel;
37import javax.swing.JPopupMenu;
38import javax.swing.JSeparator;
39
40import org.openstreetmap.josm.Main;
41import org.openstreetmap.josm.actions.ImageryAdjustAction;
42import org.openstreetmap.josm.data.ProjectionBounds;
43import org.openstreetmap.josm.data.imagery.ImageryInfo;
44import org.openstreetmap.josm.data.imagery.OffsetBookmark;
45import org.openstreetmap.josm.data.preferences.ColorProperty;
46import org.openstreetmap.josm.data.preferences.IntegerProperty;
47import org.openstreetmap.josm.gui.MenuScroller;
48import org.openstreetmap.josm.gui.widgets.UrlLabel;
49import org.openstreetmap.josm.tools.GBC;
50import org.openstreetmap.josm.tools.ImageProvider;
51import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
52import org.openstreetmap.josm.tools.Utils;
53
54public abstract class ImageryLayer extends Layer {
55
56 public static final ColorProperty PROP_FADE_COLOR = new ColorProperty(marktr("Imagery fade"), Color.white);
57 public static final IntegerProperty PROP_FADE_AMOUNT = new IntegerProperty("imagery.fade_amount", 0);
58 public static final IntegerProperty PROP_SHARPEN_LEVEL = new IntegerProperty("imagery.sharpen_level", 0);
59
60 private final List<ImageProcessor> imageProcessors = new ArrayList<>();
61
62 public static Color getFadeColor() {
63 return PROP_FADE_COLOR.get();
64 }
65
66 public static Color getFadeColorWithAlpha() {
67 Color c = PROP_FADE_COLOR.get();
68 return new Color(c.getRed(), c.getGreen(), c.getBlue(), PROP_FADE_AMOUNT.get()*255/100);
69 }
70
71 protected final ImageryInfo info;
72
73 protected Icon icon;
74
75 protected double dx;
76 protected double dy;
77
78 protected GammaImageProcessor gammaImageProcessor = new GammaImageProcessor();
79 protected SharpenImageProcessor sharpenImageProcessor = new SharpenImageProcessor();
80 protected ColorfulImageProcessor collorfulnessImageProcessor = new ColorfulImageProcessor();
81
82 private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this);
83
84 /**
85 * Constructs a new {@code ImageryLayer}.
86 * @param info imagery info
87 */
88 public ImageryLayer(ImageryInfo info) {
89 super(info.getName());
90 this.info = info;
91 if (info.getIcon() != null) {
92 icon = new ImageProvider(info.getIcon()).setOptional(true).
93 setMaxSize(ImageSizes.LAYER).get();
94 }
95 if (icon == null) {
96 icon = ImageProvider.get("imagery_small");
97 }
98 addImageProcessor(collorfulnessImageProcessor);
99 addImageProcessor(gammaImageProcessor);
100 addImageProcessor(sharpenImageProcessor);
101 sharpenImageProcessor.setSharpenLevel(1 + PROP_SHARPEN_LEVEL.get() / 2f);
102 }
103
104 public double getPPD() {
105 if (!Main.isDisplayingMapView())
106 return Main.getProjection().getDefaultZoomInPPD();
107 ProjectionBounds bounds = Main.map.mapView.getProjectionBounds();
108 return Main.map.mapView.getWidth() / (bounds.maxEast - bounds.minEast);
109 }
110
111 public double getDx() {
112 return dx;
113 }
114
115 public double getDy() {
116 return dy;
117 }
118
119 public void setOffset(double dx, double dy) {
120 this.dx = dx;
121 this.dy = dy;
122 }
123
124 public void displace(double dx, double dy) {
125 this.dx += dx;
126 this.dy += dy;
127 setOffset(this.dx, this.dy);
128 }
129
130 /**
131 * Returns imagery info.
132 * @return imagery info
133 */
134 public ImageryInfo getInfo() {
135 return info;
136 }
137
138 @Override
139 public Icon getIcon() {
140 return icon;
141 }
142
143 @Override
144 public boolean isMergable(Layer other) {
145 return false;
146 }
147
148 @Override
149 public void mergeFrom(Layer from) {
150 }
151
152 @Override
153 public Object getInfoComponent() {
154 JPanel panel = new JPanel(new GridBagLayout());
155 panel.add(new JLabel(getToolTipText()), GBC.eol());
156 if (info != null) {
157 String url = info.getUrl();
158 if (url != null) {
159 panel.add(new JLabel(tr("URL: ")), GBC.std().insets(0, 5, 2, 0));
160 panel.add(new UrlLabel(url), GBC.eol().insets(2, 5, 10, 0));
161 }
162 if (dx != 0 || dy != 0) {
163 panel.add(new JLabel(tr("Offset: ") + dx + ';' + dy), GBC.eol().insets(0, 5, 10, 0));
164 }
165 }
166 return panel;
167 }
168
169 public static ImageryLayer create(ImageryInfo info) {
170 switch(info.getImageryType()) {
171 case WMS:
172 case HTML:
173 return new WMSLayer(info);
174 case WMTS:
175 return new WMTSLayer(info);
176 case TMS:
177 case BING:
178 case SCANEX:
179 return new TMSLayer(info);
180 default:
181 throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType()));
182 }
183 }
184
185 class ApplyOffsetAction extends AbstractAction {
186 private final transient OffsetBookmark b;
187
188 ApplyOffsetAction(OffsetBookmark b) {
189 super(b.name);
190 this.b = b;
191 }
192
193 @Override
194 public void actionPerformed(ActionEvent ev) {
195 setOffset(b.dx, b.dy);
196 Main.main.menu.imageryMenu.refreshOffsetMenu();
197 Main.map.repaint();
198 }
199 }
200
201 public class OffsetAction extends AbstractAction implements LayerAction {
202 @Override
203 public void actionPerformed(ActionEvent e) {
204 // Do nothing
205 }
206
207 @Override
208 public Component createMenuComponent() {
209 return getOffsetMenuItem();
210 }
211
212 @Override
213 public boolean supportLayers(List<Layer> layers) {
214 return false;
215 }
216 }
217
218 public JMenuItem getOffsetMenuItem() {
219 JMenu subMenu = new JMenu(trc("layer", "Offset"));
220 subMenu.setIcon(ImageProvider.get("mapmode", "adjustimg"));
221 return (JMenuItem) getOffsetMenuItem(subMenu);
222 }
223
224 public JComponent getOffsetMenuItem(JComponent subMenu) {
225 JMenuItem adjustMenuItem = new JMenuItem(adjustAction);
226 if (OffsetBookmark.allBookmarks.isEmpty()) return adjustMenuItem;
227
228 subMenu.add(adjustMenuItem);
229 subMenu.add(new JSeparator());
230 boolean hasBookmarks = false;
231 int menuItemHeight = 0;
232 for (OffsetBookmark b : OffsetBookmark.allBookmarks) {
233 if (!b.isUsable(this)) {
234 continue;
235 }
236 JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b));
237 if (Utils.equalsEpsilon(b.dx, dx) && Utils.equalsEpsilon(b.dy, dy)) {
238 item.setSelected(true);
239 }
240 subMenu.add(item);
241 menuItemHeight = item.getPreferredSize().height;
242 hasBookmarks = true;
243 }
244 if (menuItemHeight > 0) {
245 if (subMenu instanceof JMenu) {
246 MenuScroller.setScrollerFor((JMenu) subMenu);
247 } else if (subMenu instanceof JPopupMenu) {
248 MenuScroller.setScrollerFor((JPopupMenu) subMenu);
249 }
250 }
251 return hasBookmarks ? subMenu : adjustMenuItem;
252 }
253
254 /**
255 * An image processor which adjusts the gamma value of an image.
256 */
257 public static class GammaImageProcessor implements ImageProcessor {
258 private double gamma = 1;
259 final short[] gammaChange = new short[256];
260 private final LookupOp op3 = new LookupOp(
261 new ShortLookupTable(0, new short[][]{gammaChange, gammaChange, gammaChange}), null);
262 private final LookupOp op4 = new LookupOp(
263 new ShortLookupTable(0, new short[][]{gammaChange, gammaChange, gammaChange, gammaChange}), null);
264
265 /**
266 * Returns the currently set gamma value.
267 * @return the currently set gamma value
268 */
269 public double getGamma() {
270 return gamma;
271 }
272
273 /**
274 * Sets a new gamma value, {@code 1} stands for no correction.
275 * @param gamma new gamma value
276 */
277 public void setGamma(double gamma) {
278 this.gamma = gamma;
279 for (int i = 0; i < 256; i++) {
280 gammaChange[i] = (short) (255 * Math.pow(i / 255., gamma));
281 }
282 }
283
284 @Override
285 public BufferedImage process(BufferedImage image) {
286 if (gamma == 1) {
287 return image;
288 }
289 try {
290 final int bands = image.getRaster().getNumBands();
291 if (image.getType() != BufferedImage.TYPE_CUSTOM && bands == 3) {
292 return op3.filter(image, null);
293 } else if (image.getType() != BufferedImage.TYPE_CUSTOM && bands == 4) {
294 return op4.filter(image, null);
295 }
296 } catch (IllegalArgumentException ignore) {
297 Main.trace(ignore);
298 }
299 final int type = image.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
300 final BufferedImage to = new BufferedImage(image.getWidth(), image.getHeight(), type);
301 to.getGraphics().drawImage(image, 0, 0, null);
302 return process(to);
303 }
304
305 @Override
306 public String toString() {
307 return "GammaImageProcessor [gamma=" + gamma + ']';
308 }
309 }
310
311 /**
312 * Sharpens or blurs the image, depending on the sharpen value.
313 * <p>
314 * A positive sharpen level means that we sharpen the image.
315 * <p>
316 * A negative sharpen level let's us blur the image. -1 is the most useful value there.
317 *
318 * @author Michael Zangl
319 */
320 public static class SharpenImageProcessor implements ImageProcessor {
321 private float sharpenLevel;
322 private ConvolveOp op;
323
324 private static float[] KERNEL_IDENTITY = new float[] {
325 0, 0, 0,
326 0, 1, 0,
327 0, 0, 0
328 };
329
330 private static float[] KERNEL_BLUR = new float[] {
331 1f / 16, 2f / 16, 1f / 16,
332 2f / 16, 4f / 16, 2f / 16,
333 1f / 16, 2f / 16, 1f / 16
334 };
335
336 private static float[] KERNEL_SHARPEN = new float[] {
337 -.5f, -1f, -.5f,
338 -1f, 7, -1f,
339 -.5f, -1f, -.5f
340 };
341
342 /**
343 * Gets the current sharpen level.
344 * @return The level.
345 */
346 public float getSharpenLevel() {
347 return sharpenLevel;
348 }
349
350 /**
351 * Sets the sharpening level.
352 * @param sharpenLevel The level. Clamped to be positive or 0.
353 */
354 public void setSharpenLevel(float sharpenLevel) {
355 if (sharpenLevel < 0) {
356 this.sharpenLevel = 0;
357 } else {
358 this.sharpenLevel = sharpenLevel;
359 }
360
361 if (this.sharpenLevel < 0.95) {
362 op = generateMixed(this.sharpenLevel, KERNEL_IDENTITY, KERNEL_BLUR);
363 } else if (this.sharpenLevel > 1.05) {
364 op = generateMixed(this.sharpenLevel - 1, KERNEL_SHARPEN, KERNEL_IDENTITY);
365 } else {
366 op = null;
367 }
368 }
369
370 private ConvolveOp generateMixed(float aFactor, float[] a, float[] b) {
371 if (a.length != 9 || b.length != 9) {
372 throw new IllegalArgumentException("Illegal kernel array length.");
373 }
374 float[] values = new float[9];
375 for (int i = 0; i < values.length; i++) {
376 values[i] = aFactor * a[i] + (1 - aFactor) * b[i];
377 }
378 return new ConvolveOp(new Kernel(3, 3, values), ConvolveOp.EDGE_NO_OP, null);
379 }
380
381 @Override
382 public BufferedImage process(BufferedImage image) {
383 if (op != null) {
384 return op.filter(image, null);
385 } else {
386 return image;
387 }
388 }
389
390 @Override
391 public String toString() {
392 return "SharpenImageProcessor [sharpenLevel=" + sharpenLevel + ']';
393 }
394 }
395
396 /**
397 * Adds or removes the colorfulness of the image.
398 *
399 * @author Michael Zangl
400 */
401 public static class ColorfulImageProcessor implements ImageProcessor {
402 private ColorfulFilter op;
403 private double colorfulness = 1;
404
405 /**
406 * Gets the colorfulness value.
407 * @return The value
408 */
409 public double getColorfulness() {
410 return colorfulness;
411 }
412
413 /**
414 * Sets the colorfulness value. Clamps it to 0+
415 * @param colorfulness The value
416 */
417 public void setColorfulness(double colorfulness) {
418 if (colorfulness < 0) {
419 this.colorfulness = 0;
420 } else {
421 this.colorfulness = colorfulness;
422 }
423
424 if (this.colorfulness < .95 || this.colorfulness > 1.05) {
425 op = new ColorfulFilter(this.colorfulness);
426 } else {
427 op = null;
428 }
429 }
430
431 @Override
432 public BufferedImage process(BufferedImage image) {
433 if (op != null) {
434 return op.filter(image, null);
435 } else {
436 return image;
437 }
438 }
439
440 @Override
441 public String toString() {
442 return "ColorfulImageProcessor [colorfulness=" + colorfulness + ']';
443 }
444 }
445
446 private static class ColorfulFilter implements BufferedImageOp {
447 private final double colorfulness;
448
449 /**
450 * Create a new colorful filter.
451 * @param colorfulness The colorfulness as defined in the {@link ColorfulImageProcessor} class.
452 */
453 ColorfulFilter(double colorfulness) {
454 this.colorfulness = colorfulness;
455 }
456
457 @Override
458 public BufferedImage filter(BufferedImage src, BufferedImage dest) {
459 if (src.getWidth() == 0 || src.getHeight() == 0) {
460 return src;
461 }
462
463 if (dest == null) {
464 dest = createCompatibleDestImage(src, null);
465 }
466 DataBuffer srcBuffer = src.getRaster().getDataBuffer();
467 DataBuffer destBuffer = dest.getRaster().getDataBuffer();
468 if (!(srcBuffer instanceof DataBufferByte) || !(destBuffer instanceof DataBufferByte)) {
469 Main.trace("Cannot apply color filter: Images do not use DataBufferByte.");
470 return src;
471 }
472
473 int type = src.getType();
474 if (type != dest.getType()) {
475 Main.trace("Cannot apply color filter: Src / Dest differ in type (" + type + '/' + dest.getType() + ')');
476 return src;
477 }
478 int redOffset, greenOffset, blueOffset, alphaOffset = 0;
479 switch (type) {
480 case BufferedImage.TYPE_3BYTE_BGR:
481 blueOffset = 0;
482 greenOffset = 1;
483 redOffset = 2;
484 break;
485 case BufferedImage.TYPE_4BYTE_ABGR:
486 case BufferedImage.TYPE_4BYTE_ABGR_PRE:
487 blueOffset = 1;
488 greenOffset = 2;
489 redOffset = 3;
490 break;
491 case BufferedImage.TYPE_INT_ARGB:
492 case BufferedImage.TYPE_INT_ARGB_PRE:
493 redOffset = 0;
494 greenOffset = 1;
495 blueOffset = 2;
496 alphaOffset = 3;
497 break;
498 default:
499 Main.trace("Cannot apply color filter: Source image is of wrong type (" + type + ").");
500 return src;
501 }
502 doFilter((DataBufferByte) srcBuffer, (DataBufferByte) destBuffer, redOffset, greenOffset, blueOffset,
503 alphaOffset, src.getAlphaRaster() != null);
504 return dest;
505 }
506
507 private void doFilter(DataBufferByte src, DataBufferByte dest, int redOffset, int greenOffset, int blueOffset,
508 int alphaOffset, boolean hasAlpha) {
509 byte[] srcPixels = src.getData();
510 byte[] destPixels = dest.getData();
511 if (srcPixels.length != destPixels.length) {
512 Main.trace("Cannot apply color filter: Source/Dest lengths differ.");
513 return;
514 }
515 int entries = hasAlpha ? 4 : 3;
516 for (int i = 0; i < srcPixels.length; i += entries) {
517 int r = srcPixels[i + redOffset] & 0xff;
518 int g = srcPixels[i + greenOffset] & 0xff;
519 int b = srcPixels[i + blueOffset] & 0xff;
520 double luminosity = r * .21d + g * .72d + b * .07d;
521 destPixels[i + redOffset] = mix(r, luminosity);
522 destPixels[i + greenOffset] = mix(g, luminosity);
523 destPixels[i + blueOffset] = mix(b, luminosity);
524 if (hasAlpha) {
525 destPixels[i + alphaOffset] = srcPixels[i + alphaOffset];
526 }
527 }
528 }
529
530 private byte mix(int color, double luminosity) {
531 int val = (int) (colorfulness * color + (1 - colorfulness) * luminosity);
532 if (val < 0) {
533 return 0;
534 } else if (val > 0xff) {
535 return (byte) 0xff;
536 } else {
537 return (byte) val;
538 }
539 }
540
541 @Override
542 public Rectangle2D getBounds2D(BufferedImage src) {
543 return new Rectangle(src.getWidth(), src.getHeight());
544 }
545
546 @Override
547 public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) {
548 return new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
549 }
550
551 @Override
552 public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
553 return (Point2D) srcPt.clone();
554 }
555
556 @Override
557 public RenderingHints getRenderingHints() {
558 return null;
559 }
560
561 }
562
563 /**
564 * Returns the currently set gamma value.
565 * @return the currently set gamma value
566 */
567 public double getGamma() {
568 return gammaImageProcessor.getGamma();
569 }
570
571 /**
572 * Sets a new gamma value, {@code 1} stands for no correction.
573 * @param gamma new gamma value
574 */
575 public void setGamma(double gamma) {
576 gammaImageProcessor.setGamma(gamma);
577 }
578
579 /**
580 * Gets the current sharpen level.
581 * @return The sharpen level.
582 */
583 public double getSharpenLevel() {
584 return sharpenImageProcessor.getSharpenLevel();
585 }
586
587 /**
588 * Sets the sharpen level for the layer.
589 * <code>1</code> means no change in sharpness.
590 * Values in range 0..1 blur the image.
591 * Values above 1 are used to sharpen the image.
592 * @param sharpenLevel The sharpen level.
593 */
594 public void setSharpenLevel(double sharpenLevel) {
595 sharpenImageProcessor.setSharpenLevel((float) sharpenLevel);
596 }
597
598 /**
599 * Gets the colorfulness of this image.
600 * @return The colorfulness
601 */
602 public double getColorfulness() {
603 return collorfulnessImageProcessor.getColorfulness();
604 }
605
606 /**
607 * Sets the colorfulness of this image.
608 * 0 means grayscale.
609 * 1 means normal colorfulness.
610 * Values greater than 1 are allowed.
611 * @param colorfulness The colorfulness.
612 */
613 public void setColorfulness(double colorfulness) {
614 collorfulnessImageProcessor.setColorfulness(colorfulness);
615 }
616
617 /**
618 * This method adds the {@link ImageProcessor} to this Layer if it is not {@code null}.
619 *
620 * @param processor that processes the image
621 *
622 * @return true if processor was added, false otherwise
623 */
624 public boolean addImageProcessor(ImageProcessor processor) {
625 return processor != null && imageProcessors.add(processor);
626 }
627
628 /**
629 * This method removes given {@link ImageProcessor} from this layer
630 *
631 * @param processor which is needed to be removed
632 *
633 * @return true if processor was removed
634 */
635 public boolean removeImageProcessor(ImageProcessor processor) {
636 return imageProcessors.remove(processor);
637 }
638
639 /**
640 * Wraps a {@link BufferedImageOp} to be used as {@link ImageProcessor}.
641 * @param op the {@link BufferedImageOp}
642 * @param inPlace true to apply filter in place, i.e., not create a new {@link BufferedImage} for the result
643 * (the {@code op} needs to support this!)
644 * @return the {@link ImageProcessor} wrapper
645 */
646 public static ImageProcessor createImageProcessor(final BufferedImageOp op, final boolean inPlace) {
647 return new ImageProcessor() {
648 @Override
649 public BufferedImage process(BufferedImage image) {
650 return op.filter(image, inPlace ? image : null);
651 }
652 };
653 }
654
655 /**
656 * This method gets all {@link ImageProcessor}s of the layer
657 *
658 * @return list of image processors without removed one
659 */
660 public List<ImageProcessor> getImageProcessors() {
661 return imageProcessors;
662 }
663
664 /**
665 * Applies all the chosen {@link ImageProcessor}s to the image
666 *
667 * @param img - image which should be changed
668 *
669 * @return the new changed image
670 */
671 public BufferedImage applyImageProcessors(BufferedImage img) {
672 for (ImageProcessor processor : imageProcessors) {
673 img = processor.process(img);
674 }
675 return img;
676 }
677
678 @Override
679 public void destroy() {
680 super.destroy();
681 adjustAction.destroy();
682 }
683}
Note: See TracBrowser for help on using the repository browser.