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 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}