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.Font; 011import java.awt.Graphics2D; 012import java.awt.GridBagLayout; 013import java.awt.Transparency; 014import java.awt.event.ActionEvent; 015import java.awt.font.FontRenderContext; 016import java.awt.font.LineBreakMeasurer; 017import java.awt.font.TextAttribute; 018import java.awt.font.TextLayout; 019import java.awt.image.BufferedImage; 020import java.awt.image.BufferedImageOp; 021import java.awt.image.ConvolveOp; 022import java.awt.image.Kernel; 023import java.awt.image.LookupOp; 024import java.awt.image.ShortLookupTable; 025import java.text.AttributedCharacterIterator; 026import java.text.AttributedString; 027import java.util.ArrayList; 028import java.util.HashMap; 029import java.util.List; 030import java.util.Map; 031 032import javax.swing.AbstractAction; 033import javax.swing.Icon; 034import javax.swing.JCheckBoxMenuItem; 035import javax.swing.JComponent; 036import javax.swing.JLabel; 037import javax.swing.JMenu; 038import javax.swing.JMenuItem; 039import javax.swing.JPanel; 040import javax.swing.JPopupMenu; 041import javax.swing.JSeparator; 042 043import org.openstreetmap.josm.Main; 044import org.openstreetmap.josm.actions.ImageryAdjustAction; 045import org.openstreetmap.josm.data.ProjectionBounds; 046import org.openstreetmap.josm.data.imagery.ImageryInfo; 047import org.openstreetmap.josm.data.imagery.OffsetBookmark; 048import org.openstreetmap.josm.data.preferences.ColorProperty; 049import org.openstreetmap.josm.data.preferences.IntegerProperty; 050import org.openstreetmap.josm.gui.MenuScroller; 051import org.openstreetmap.josm.gui.widgets.UrlLabel; 052import org.openstreetmap.josm.tools.GBC; 053import org.openstreetmap.josm.tools.ImageProvider; 054import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; 055import org.openstreetmap.josm.tools.Utils; 056 057public abstract class ImageryLayer extends Layer { 058 059 public static final ColorProperty PROP_FADE_COLOR = new ColorProperty(marktr("Imagery fade"), Color.white); 060 public static final IntegerProperty PROP_FADE_AMOUNT = new IntegerProperty("imagery.fade_amount", 0); 061 public static final IntegerProperty PROP_SHARPEN_LEVEL = new IntegerProperty("imagery.sharpen_level", 0); 062 063 private final List<ImageProcessor> imageProcessors = new ArrayList<>(); 064 065 public static Color getFadeColor() { 066 return PROP_FADE_COLOR.get(); 067 } 068 069 public static Color getFadeColorWithAlpha() { 070 Color c = PROP_FADE_COLOR.get(); 071 return new Color(c.getRed(), c.getGreen(), c.getBlue(), PROP_FADE_AMOUNT.get()*255/100); 072 } 073 074 protected final ImageryInfo info; 075 076 protected Icon icon; 077 078 protected double dx; 079 protected double dy; 080 081 protected GammaImageProcessor gammaImageProcessor = new GammaImageProcessor(); 082 083 private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this); 084 085 /** 086 * Constructs a new {@code ImageryLayer}. 087 * @param info imagery info 088 */ 089 public ImageryLayer(ImageryInfo info) { 090 super(info.getName()); 091 this.info = info; 092 if (info.getIcon() != null) { 093 icon = new ImageProvider(info.getIcon()).setOptional(true). 094 setMaxSize(ImageSizes.LAYER).get(); 095 } 096 if (icon == null) { 097 icon = ImageProvider.get("imagery_small"); 098 } 099 addImageProcessor(createSharpener(PROP_SHARPEN_LEVEL.get())); 100 addImageProcessor(gammaImageProcessor); 101 } 102 103 public double getPPD() { 104 if (!Main.isDisplayingMapView()) return Main.getProjection().getDefaultZoomInPPD(); 105 ProjectionBounds bounds = Main.map.mapView.getProjectionBounds(); 106 return Main.map.mapView.getWidth() / (bounds.maxEast - bounds.minEast); 107 } 108 109 public double getDx() { 110 return dx; 111 } 112 113 public double getDy() { 114 return dy; 115 } 116 117 public void setOffset(double dx, double dy) { 118 this.dx = dx; 119 this.dy = dy; 120 } 121 122 public void displace(double dx, double dy) { 123 setOffset(this.dx += dx, this.dy += dy); 124 } 125 126 public ImageryInfo getInfo() { 127 return info; 128 } 129 130 @Override 131 public Icon getIcon() { 132 return icon; 133 } 134 135 @Override 136 public boolean isMergable(Layer other) { 137 return false; 138 } 139 140 @Override 141 public void mergeFrom(Layer from) { 142 } 143 144 @Override 145 public Object getInfoComponent() { 146 JPanel panel = new JPanel(new GridBagLayout()); 147 panel.add(new JLabel(getToolTipText()), GBC.eol()); 148 if (info != null) { 149 String url = info.getUrl(); 150 if (url != null) { 151 panel.add(new JLabel(tr("URL: ")), GBC.std().insets(0, 5, 2, 0)); 152 panel.add(new UrlLabel(url), GBC.eol().insets(2, 5, 10, 0)); 153 } 154 if (dx != 0 || dy != 0) { 155 panel.add(new JLabel(tr("Offset: ") + dx + ';' + dy), GBC.eol().insets(0, 5, 10, 0)); 156 } 157 } 158 return panel; 159 } 160 161 public static ImageryLayer create(ImageryInfo info) { 162 switch(info.getImageryType()) { 163 case WMS: 164 case HTML: 165 return new WMSLayer(info); 166 case WMTS: 167 return new WMTSLayer(info); 168 case TMS: 169 case BING: 170 case SCANEX: 171 return new TMSLayer(info); 172 default: 173 throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType())); 174 } 175 } 176 177 class ApplyOffsetAction extends AbstractAction { 178 private final transient OffsetBookmark b; 179 180 ApplyOffsetAction(OffsetBookmark b) { 181 super(b.name); 182 this.b = b; 183 } 184 185 @Override 186 public void actionPerformed(ActionEvent ev) { 187 setOffset(b.dx, b.dy); 188 Main.main.menu.imageryMenu.refreshOffsetMenu(); 189 Main.map.repaint(); 190 } 191 } 192 193 public class OffsetAction extends AbstractAction implements LayerAction { 194 @Override 195 public void actionPerformed(ActionEvent e) { 196 } 197 198 @Override 199 public Component createMenuComponent() { 200 return getOffsetMenuItem(); 201 } 202 203 @Override 204 public boolean supportLayers(List<Layer> layers) { 205 return false; 206 } 207 } 208 209 public JMenuItem getOffsetMenuItem() { 210 JMenu subMenu = new JMenu(trc("layer", "Offset")); 211 subMenu.setIcon(ImageProvider.get("mapmode", "adjustimg")); 212 return (JMenuItem) getOffsetMenuItem(subMenu); 213 } 214 215 public JComponent getOffsetMenuItem(JComponent subMenu) { 216 JMenuItem adjustMenuItem = new JMenuItem(adjustAction); 217 if (OffsetBookmark.allBookmarks.isEmpty()) return adjustMenuItem; 218 219 subMenu.add(adjustMenuItem); 220 subMenu.add(new JSeparator()); 221 boolean hasBookmarks = false; 222 int menuItemHeight = 0; 223 for (OffsetBookmark b : OffsetBookmark.allBookmarks) { 224 if (!b.isUsable(this)) { 225 continue; 226 } 227 JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b)); 228 if (Utils.equalsEpsilon(b.dx, dx) && Utils.equalsEpsilon(b.dy, dy)) { 229 item.setSelected(true); 230 } 231 subMenu.add(item); 232 menuItemHeight = item.getPreferredSize().height; 233 hasBookmarks = true; 234 } 235 if (menuItemHeight > 0) { 236 if (subMenu instanceof JMenu) { 237 MenuScroller.setScrollerFor((JMenu) subMenu); 238 } else if (subMenu instanceof JPopupMenu) { 239 MenuScroller.setScrollerFor((JPopupMenu) subMenu); 240 } 241 } 242 return hasBookmarks ? subMenu : adjustMenuItem; 243 } 244 245 public ImageProcessor createSharpener(int sharpenLevel) { 246 final Kernel kernel; 247 if (sharpenLevel == 1) { 248 kernel = new Kernel(3, 3, new float[]{-0.25f, -0.5f, -0.25f, -0.5f, 4, -0.5f, -0.25f, -0.5f, -0.25f}); 249 } else if (sharpenLevel == 2) { 250 kernel = new Kernel(3, 3, new float[]{-0.5f, -1, -0.5f, -1, 7, -1, -0.5f, -1, -0.5f}); 251 } else { 252 return null; 253 } 254 BufferedImageOp op = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null); 255 return createImageProcessor(op, false); 256 } 257 258 /** 259 * An image processor which adjusts the gamma value of an image. 260 */ 261 public static class GammaImageProcessor implements ImageProcessor { 262 private double gamma = 1; 263 final short[] gammaChange = new short[256]; 264 private final LookupOp op3 = new LookupOp( 265 new ShortLookupTable(0, new short[][]{gammaChange, gammaChange, gammaChange}), null); 266 private final LookupOp op4 = new LookupOp( 267 new ShortLookupTable(0, new short[][]{gammaChange, gammaChange, gammaChange, gammaChange}), null); 268 269 /** 270 * Returns the currently set gamma value. 271 * @return the currently set gamma value 272 */ 273 public double getGamma() { 274 return gamma; 275 } 276 277 /** 278 * Sets a new gamma value, {@code 1} stands for no correction. 279 * @param gamma new gamma value 280 */ 281 public void setGamma(double gamma) { 282 this.gamma = gamma; 283 for (int i = 0; i < 256; i++) { 284 gammaChange[i] = (short) (255 * Math.pow(i / 255., gamma)); 285 } 286 } 287 288 @Override 289 public BufferedImage process(BufferedImage image) { 290 if (gamma == 1) { 291 return image; 292 } 293 try { 294 final int bands = image.getRaster().getNumBands(); 295 if (image.getType() != BufferedImage.TYPE_CUSTOM && bands == 3) { 296 return op3.filter(image, null); 297 } else if (image.getType() != BufferedImage.TYPE_CUSTOM && bands == 4) { 298 return op4.filter(image, null); 299 } 300 } catch (IllegalArgumentException ignore) { 301 if (Main.isTraceEnabled()) { 302 Main.trace(ignore.getMessage()); 303 } 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 312 /** 313 * Returns the currently set gamma value. 314 * @return the currently set gamma value 315 */ 316 public double getGamma() { 317 return gammaImageProcessor.getGamma(); 318 } 319 320 /** 321 * Sets a new gamma value, {@code 1} stands for no correction. 322 * @param gamma new gamma value 323 */ 324 public void setGamma(double gamma) { 325 gammaImageProcessor.setGamma(gamma); 326 } 327 328 /** 329 * This method adds the {@link ImageProcessor} to this Layer if it is not {@code null}. 330 * 331 * @param processor that processes the image 332 * 333 * @return true if processor was added, false otherwise 334 */ 335 public boolean addImageProcessor(ImageProcessor processor) { 336 return processor != null && imageProcessors.add(processor); 337 } 338 339 /** 340 * This method removes given {@link ImageProcessor} from this layer 341 * 342 * @param processor which is needed to be removed 343 * 344 * @return true if processor was removed 345 */ 346 public boolean removeImageProcessor(ImageProcessor processor) { 347 return imageProcessors.remove(processor); 348 } 349 350 /** 351 * Wraps a {@link BufferedImageOp} to be used as {@link ImageProcessor}. 352 * @param op the {@link BufferedImageOp} 353 * @param inPlace true to apply filter in place, i.e., not create a new {@link BufferedImage} for the result 354 * (the {@code op} needs to support this!) 355 * @return the {@link ImageProcessor} wrapper 356 */ 357 public static ImageProcessor createImageProcessor(final BufferedImageOp op, final boolean inPlace) { 358 return new ImageProcessor() { 359 @Override 360 public BufferedImage process(BufferedImage image) { 361 return op.filter(image, inPlace ? image : null); 362 } 363 }; 364 } 365 366 /** 367 * This method gets all {@link ImageProcessor}s of the layer 368 * 369 * @return list of image processors without removed one 370 */ 371 public List<ImageProcessor> getImageProcessors() { 372 return imageProcessors; 373 } 374 375 /** 376 * Applies all the chosen {@link ImageProcessor}s to the image 377 * 378 * @param img - image which should be changed 379 * 380 * @return the new changed image 381 */ 382 public BufferedImage applyImageProcessors(BufferedImage img) { 383 for (ImageProcessor processor : imageProcessors) { 384 img = processor.process(img); 385 } 386 return img; 387 } 388 389 /** 390 * Draws a red error tile when imagery tile cannot be fetched. 391 * @param img The buffered image 392 * @param message Additional error message to display 393 */ 394 public void drawErrorTile(BufferedImage img, String message) { 395 Graphics2D g = (Graphics2D) img.getGraphics(); 396 g.setColor(Color.RED); 397 g.fillRect(0, 0, img.getWidth(), img.getHeight()); 398 g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(24.0f)); 399 g.setColor(Color.BLACK); 400 401 String text = tr("ERROR"); 402 g.drawString(text, (img.getWidth() - g.getFontMetrics().stringWidth(text)) / 2, g.getFontMetrics().getHeight()+5); 403 if (message != null) { 404 float drawPosY = 2.5f*g.getFontMetrics().getHeight()+10; 405 if (!message.contains(" ")) { 406 g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(18.0f)); 407 g.drawString(message, 5, (int) drawPosY); 408 } else { 409 // Draw message on several lines 410 Map<TextAttribute, Object> map = new HashMap<>(); 411 map.put(TextAttribute.FAMILY, "Serif"); 412 map.put(TextAttribute.SIZE, new Float(18.0)); 413 AttributedString vanGogh = new AttributedString(message, map); 414 // Create a new LineBreakMeasurer from the text 415 AttributedCharacterIterator paragraph = vanGogh.getIterator(); 416 int paragraphStart = paragraph.getBeginIndex(); 417 int paragraphEnd = paragraph.getEndIndex(); 418 FontRenderContext frc = g.getFontRenderContext(); 419 LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc); 420 // Set break width to width of image with some margin 421 float breakWidth = img.getWidth()-10; 422 // Set position to the index of the first character in the text 423 lineMeasurer.setPosition(paragraphStart); 424 // Get lines until the entire paragraph has been displayed 425 while (lineMeasurer.getPosition() < paragraphEnd) { 426 // Retrieve next layout 427 TextLayout layout = lineMeasurer.nextLayout(breakWidth); 428 429 // Compute pen x position 430 float drawPosX = layout.isLeftToRight() ? 0 : breakWidth - layout.getAdvance(); 431 432 // Move y-coordinate by the ascent of the layout 433 drawPosY += layout.getAscent(); 434 435 // Draw the TextLayout at (drawPosX, drawPosY) 436 layout.draw(g, drawPosX, drawPosY); 437 438 // Move y-coordinate in preparation for next layout 439 drawPosY += layout.getDescent() + layout.getLeading(); 440 } 441 } 442 } 443 } 444 445 @Override 446 public void destroy() { 447 super.destroy(); 448 adjustAction.destroy(); 449 } 450}