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