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