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

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

sonar - squid:S3052 - Fields should not be initialized to default values

  • Property svn:eol-style set to native
File size: 22.2 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 if (Main.isTraceEnabled()) {
298 Main.trace(ignore.getMessage());
299 }
300 }
301 final int type = image.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
302 final BufferedImage to = new BufferedImage(image.getWidth(), image.getHeight(), type);
303 to.getGraphics().drawImage(image, 0, 0, null);
304 return process(to);
305 }
306
307 @Override
308 public String toString() {
309 return "GammaImageProcessor [gamma=" + gamma + ']';
310 }
311 }
312
313 /**
314 * Sharpens or blurs the image, depending on the sharpen value.
315 * <p>
316 * A positive sharpen level means that we sharpen the image.
317 * <p>
318 * A negative sharpen level let's us blur the image. -1 is the most useful value there.
319 *
320 * @author Michael Zangl
321 */
322 public static class SharpenImageProcessor implements ImageProcessor {
323 private float sharpenLevel;
324 private ConvolveOp op;
325
326 private static float[] KERNEL_IDENTITY = new float[] {
327 0, 0, 0,
328 0, 1, 0,
329 0, 0, 0
330 };
331
332 private static float[] KERNEL_BLUR = new float[] {
333 1f / 16, 2f / 16, 1f / 16,
334 2f / 16, 4f / 16, 2f / 16,
335 1f / 16, 2f / 16, 1f / 16
336 };
337
338 private static float[] KERNEL_SHARPEN = new float[] {
339 -.5f, -1f, -.5f,
340 -1f, 7, -1f,
341 -.5f, -1f, -.5f
342 };
343
344 /**
345 * Gets the current sharpen level.
346 * @return The level.
347 */
348 public float getSharpenLevel() {
349 return sharpenLevel;
350 }
351
352 /**
353 * Sets the sharpening level.
354 * @param sharpenLevel The level. Clamped to be positive or 0.
355 */
356 public void setSharpenLevel(float sharpenLevel) {
357 if (sharpenLevel < 0) {
358 this.sharpenLevel = 0;
359 } else {
360 this.sharpenLevel = sharpenLevel;
361 }
362
363 if (this.sharpenLevel < 0.95) {
364 op = generateMixed(this.sharpenLevel, KERNEL_IDENTITY, KERNEL_BLUR);
365 } else if (this.sharpenLevel > 1.05) {
366 op = generateMixed(this.sharpenLevel - 1, KERNEL_SHARPEN, KERNEL_IDENTITY);
367 } else {
368 op = null;
369 }
370 }
371
372 private ConvolveOp generateMixed(float aFactor, float[] a, float[] b) {
373 if (a.length != 9 || b.length != 9) {
374 throw new IllegalArgumentException("Illegal kernel array length.");
375 }
376 float[] values = new float[9];
377 for (int i = 0; i < values.length; i++) {
378 values[i] = aFactor * a[i] + (1 - aFactor) * b[i];
379 }
380 return new ConvolveOp(new Kernel(3, 3, values), ConvolveOp.EDGE_NO_OP, null);
381 }
382
383 @Override
384 public BufferedImage process(BufferedImage image) {
385 if (op != null) {
386 return op.filter(image, null);
387 } else {
388 return image;
389 }
390 }
391
392 @Override
393 public String toString() {
394 return "SharpenImageProcessor [sharpenLevel=" + sharpenLevel + ']';
395 }
396 }
397
398 /**
399 * Adds or removes the colorfulness of the image.
400 *
401 * @author Michael Zangl
402 */
403 public static class ColorfulImageProcessor implements ImageProcessor {
404 private ColorfulFilter op;
405 private double colorfulness = 1;
406
407 /**
408 * Gets the colorfulness value.
409 * @return The value
410 */
411 public double getColorfulness() {
412 return colorfulness;
413 }
414
415 /**
416 * Sets the colorfulness value. Clamps it to 0+
417 * @param colorfulness The value
418 */
419 public void setColorfulness(double colorfulness) {
420 if (colorfulness < 0) {
421 this.colorfulness = 0;
422 } else {
423 this.colorfulness = colorfulness;
424 }
425
426 if (this.colorfulness < .95 || this.colorfulness > 1.05) {
427 op = new ColorfulFilter(this.colorfulness);
428 } else {
429 op = null;
430 }
431 }
432
433 @Override
434 public BufferedImage process(BufferedImage image) {
435 if (op != null) {
436 return op.filter(image, null);
437 } else {
438 return image;
439 }
440 }
441
442 @Override
443 public String toString() {
444 return "ColorfulImageProcessor [colorfulness=" + colorfulness + ']';
445 }
446 }
447
448 private static class ColorfulFilter implements BufferedImageOp {
449 private final double colorfulness;
450
451 /**
452 * Create a new colorful filter.
453 * @param colorfulness The colorfulness as defined in the {@link ColorfulImageProcessor} class.
454 */
455 ColorfulFilter(double colorfulness) {
456 this.colorfulness = colorfulness;
457 }
458
459 @Override
460 public BufferedImage filter(BufferedImage src, BufferedImage dest) {
461 if (src.getWidth() == 0 || src.getHeight() == 0) {
462 return src;
463 }
464
465 if (dest == null) {
466 dest = createCompatibleDestImage(src, null);
467 }
468 DataBuffer srcBuffer = src.getRaster().getDataBuffer();
469 DataBuffer destBuffer = dest.getRaster().getDataBuffer();
470 if (!(srcBuffer instanceof DataBufferByte) || !(destBuffer instanceof DataBufferByte)) {
471 Main.trace("Cannot apply color filter: Images do not use DataBufferByte.");
472 return src;
473 }
474
475 int type = src.getType();
476 if (type != dest.getType()) {
477 Main.trace("Cannot apply color filter: Src / Dest differ in type (" + type + '/' + dest.getType() + ')');
478 return src;
479 }
480 int redOffset, greenOffset, blueOffset, alphaOffset = 0;
481 switch (type) {
482 case BufferedImage.TYPE_3BYTE_BGR:
483 blueOffset = 0;
484 greenOffset = 1;
485 redOffset = 2;
486 break;
487 case BufferedImage.TYPE_4BYTE_ABGR:
488 case BufferedImage.TYPE_4BYTE_ABGR_PRE:
489 blueOffset = 1;
490 greenOffset = 2;
491 redOffset = 3;
492 break;
493 case BufferedImage.TYPE_INT_ARGB:
494 case BufferedImage.TYPE_INT_ARGB_PRE:
495 redOffset = 0;
496 greenOffset = 1;
497 blueOffset = 2;
498 alphaOffset = 3;
499 break;
500 default:
501 Main.trace("Cannot apply color filter: Source image is of wrong type (" + type + ").");
502 return src;
503 }
504 doFilter((DataBufferByte) srcBuffer, (DataBufferByte) destBuffer, redOffset, greenOffset, blueOffset,
505 alphaOffset, src.getAlphaRaster() != null);
506 return dest;
507 }
508
509 private void doFilter(DataBufferByte src, DataBufferByte dest, int redOffset, int greenOffset, int blueOffset,
510 int alphaOffset, boolean hasAlpha) {
511 byte[] srcPixels = src.getData();
512 byte[] destPixels = dest.getData();
513 if (srcPixels.length != destPixels.length) {
514 Main.trace("Cannot apply color filter: Source/Dest lengths differ.");
515 return;
516 }
517 int entries = hasAlpha ? 4 : 3;
518 for (int i = 0; i < srcPixels.length; i += entries) {
519 int r = srcPixels[i + redOffset] & 0xff;
520 int g = srcPixels[i + greenOffset] & 0xff;
521 int b = srcPixels[i + blueOffset] & 0xff;
522 double luminosity = r * .21d + g * .72d + b * .07d;
523 destPixels[i + redOffset] = mix(r, luminosity);
524 destPixels[i + greenOffset] = mix(g, luminosity);
525 destPixels[i + blueOffset] = mix(b, luminosity);
526 if (hasAlpha) {
527 destPixels[i + alphaOffset] = srcPixels[i + alphaOffset];
528 }
529 }
530 }
531
532 private byte mix(int color, double luminosity) {
533 int val = (int) (colorfulness * color + (1 - colorfulness) * luminosity);
534 if (val < 0) {
535 return 0;
536 } else if (val > 0xff) {
537 return (byte) 0xff;
538 } else {
539 return (byte) val;
540 }
541 }
542
543 @Override
544 public Rectangle2D getBounds2D(BufferedImage src) {
545 return new Rectangle(src.getWidth(), src.getHeight());
546 }
547
548 @Override
549 public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) {
550 return new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
551 }
552
553 @Override
554 public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
555 return (Point2D) srcPt.clone();
556 }
557
558 @Override
559 public RenderingHints getRenderingHints() {
560 return null;
561 }
562
563 }
564
565 /**
566 * Returns the currently set gamma value.
567 * @return the currently set gamma value
568 */
569 public double getGamma() {
570 return gammaImageProcessor.getGamma();
571 }
572
573 /**
574 * Sets a new gamma value, {@code 1} stands for no correction.
575 * @param gamma new gamma value
576 */
577 public void setGamma(double gamma) {
578 gammaImageProcessor.setGamma(gamma);
579 }
580
581 /**
582 * Gets the current sharpen level.
583 * @return The sharpen level.
584 */
585 public double getSharpenLevel() {
586 return sharpenImageProcessor.getSharpenLevel();
587 }
588
589 /**
590 * Sets the sharpen level for the layer.
591 * <code>1</code> means no change in sharpness.
592 * Values in range 0..1 blur the image.
593 * Values above 1 are used to sharpen the image.
594 * @param sharpenLevel The sharpen level.
595 */
596 public void setSharpenLevel(double sharpenLevel) {
597 sharpenImageProcessor.setSharpenLevel((float) sharpenLevel);
598 }
599
600 /**
601 * Gets the colorfulness of this image.
602 * @return The colorfulness
603 */
604 public double getColorfulness() {
605 return collorfulnessImageProcessor.getColorfulness();
606 }
607
608 /**
609 * Sets the colorfulness of this image.
610 * 0 means grayscale.
611 * 1 means normal colorfulness.
612 * Values greater than 1 are allowed.
613 * @param colorfulness The colorfulness.
614 */
615 public void setColorfulness(double colorfulness) {
616 collorfulnessImageProcessor.setColorfulness(colorfulness);
617 }
618
619 /**
620 * This method adds the {@link ImageProcessor} to this Layer if it is not {@code null}.
621 *
622 * @param processor that processes the image
623 *
624 * @return true if processor was added, false otherwise
625 */
626 public boolean addImageProcessor(ImageProcessor processor) {
627 return processor != null && imageProcessors.add(processor);
628 }
629
630 /**
631 * This method removes given {@link ImageProcessor} from this layer
632 *
633 * @param processor which is needed to be removed
634 *
635 * @return true if processor was removed
636 */
637 public boolean removeImageProcessor(ImageProcessor processor) {
638 return imageProcessors.remove(processor);
639 }
640
641 /**
642 * Wraps a {@link BufferedImageOp} to be used as {@link ImageProcessor}.
643 * @param op the {@link BufferedImageOp}
644 * @param inPlace true to apply filter in place, i.e., not create a new {@link BufferedImage} for the result
645 * (the {@code op} needs to support this!)
646 * @return the {@link ImageProcessor} wrapper
647 */
648 public static ImageProcessor createImageProcessor(final BufferedImageOp op, final boolean inPlace) {
649 return new ImageProcessor() {
650 @Override
651 public BufferedImage process(BufferedImage image) {
652 return op.filter(image, inPlace ? image : null);
653 }
654 };
655 }
656
657 /**
658 * This method gets all {@link ImageProcessor}s of the layer
659 *
660 * @return list of image processors without removed one
661 */
662 public List<ImageProcessor> getImageProcessors() {
663 return imageProcessors;
664 }
665
666 /**
667 * Applies all the chosen {@link ImageProcessor}s to the image
668 *
669 * @param img - image which should be changed
670 *
671 * @return the new changed image
672 */
673 public BufferedImage applyImageProcessors(BufferedImage img) {
674 for (ImageProcessor processor : imageProcessors) {
675 img = processor.process(img);
676 }
677 return img;
678 }
679
680 @Override
681 public void destroy() {
682 super.destroy();
683 adjustAction.destroy();
684 }
685}
Note: See TracBrowser for help on using the repository browser.