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

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

sonar - squid:AssignmentInSubExpressionCheck - Assignments should not be made from within sub-expressions

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