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

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

sonar - squid:S1186 - Methods should not be empty

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