001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trc; 006 007import java.awt.Component; 008import java.awt.GridBagLayout; 009import java.awt.event.ActionEvent; 010import java.awt.image.BufferedImage; 011import java.awt.image.BufferedImageOp; 012import java.util.ArrayList; 013import java.util.Arrays; 014import java.util.List; 015import java.util.Locale; 016 017import javax.swing.AbstractAction; 018import javax.swing.Action; 019import javax.swing.BorderFactory; 020import javax.swing.Icon; 021import javax.swing.JCheckBoxMenuItem; 022import javax.swing.JComponent; 023import javax.swing.JLabel; 024import javax.swing.JMenu; 025import javax.swing.JMenuItem; 026import javax.swing.JPanel; 027import javax.swing.JPopupMenu; 028import javax.swing.JSeparator; 029import javax.swing.JTextField; 030 031import org.openstreetmap.josm.Main; 032import org.openstreetmap.josm.data.ProjectionBounds; 033import org.openstreetmap.josm.data.imagery.ImageryInfo; 034import org.openstreetmap.josm.data.imagery.OffsetBookmark; 035import org.openstreetmap.josm.data.preferences.IntegerProperty; 036import org.openstreetmap.josm.gui.MainApplication; 037import org.openstreetmap.josm.gui.MapView; 038import org.openstreetmap.josm.gui.MenuScroller; 039import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings; 040import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings; 041import org.openstreetmap.josm.gui.widgets.UrlLabel; 042import org.openstreetmap.josm.tools.GBC; 043import org.openstreetmap.josm.tools.ImageProcessor; 044import org.openstreetmap.josm.tools.ImageProvider; 045import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; 046 047/** 048 * Abstract base class for background imagery layers ({@link WMSLayer}, {@link TMSLayer}, {@link WMTSLayer}). 049 * 050 * Handles some common tasks, like image filters, image processors, etc. 051 */ 052public abstract class ImageryLayer extends Layer { 053 054 /** 055 * The default value for the sharpen filter for each imagery layer. 056 */ 057 public static final IntegerProperty PROP_SHARPEN_LEVEL = new IntegerProperty("imagery.sharpen_level", 0); 058 059 private final List<ImageProcessor> imageProcessors = new ArrayList<>(); 060 061 protected final ImageryInfo info; 062 063 protected Icon icon; 064 065 private final ImageryFilterSettings filterSettings = new ImageryFilterSettings(); 066 067 /** 068 * Constructs a new {@code ImageryLayer}. 069 * @param info imagery info 070 */ 071 public ImageryLayer(ImageryInfo info) { 072 super(info.getName()); 073 this.info = info; 074 if (info.getIcon() != null) { 075 icon = new ImageProvider(info.getIcon()).setOptional(true). 076 setMaxSize(ImageSizes.LAYER).get(); 077 } 078 if (icon == null) { 079 icon = ImageProvider.get("imagery_small"); 080 } 081 for (ImageProcessor processor : filterSettings.getProcessors()) { 082 addImageProcessor(processor); 083 } 084 filterSettings.setSharpenLevel(1 + PROP_SHARPEN_LEVEL.get() / 2f); 085 } 086 087 public double getPPD() { 088 if (!MainApplication.isDisplayingMapView()) 089 return Main.getProjection().getDefaultZoomInPPD(); 090 MapView mapView = MainApplication.getMap().mapView; 091 ProjectionBounds bounds = mapView.getProjectionBounds(); 092 return mapView.getWidth() / (bounds.maxEast - bounds.minEast); 093 } 094 095 /** 096 * Gets the x displacement of this layer. 097 * To be removed end of 2016 098 * @return The x displacement. 099 * @deprecated Use {@link TileSourceDisplaySettings#getDx()} 100 */ 101 @Deprecated 102 public double getDx() { 103 // moved to AbstractTileSourceLayer/TileSourceDisplaySettings. Remains until all actions migrate. 104 return 0; 105 } 106 107 /** 108 * Gets the y displacement of this layer. 109 * To be removed end of 2016 110 * @return The y displacement. 111 * @deprecated Use {@link TileSourceDisplaySettings#getDy()} 112 */ 113 @Deprecated 114 public double getDy() { 115 // moved to AbstractTileSourceLayer/TileSourceDisplaySettings. Remains until all actions migrate. 116 return 0; 117 } 118 119 /** 120 * Sets the displacement offset of this layer. The layer is automatically invalidated. 121 * To be removed end of 2016 122 * @param offset the offset bookmark 123 * @deprecated Use {@link TileSourceDisplaySettings} 124 */ 125 @Deprecated 126 public void setOffset(OffsetBookmark offset) { 127 // moved to AbstractTileSourceLayer/TileSourceDisplaySettings. Remains until all actions migrate. 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 List<List<String>> content = new ArrayList<>(); 158 content.add(Arrays.asList(tr("Name"), info.getName())); 159 content.add(Arrays.asList(tr("Type"), info.getImageryType().getTypeString().toUpperCase(Locale.ENGLISH))); 160 content.add(Arrays.asList(tr("URL"), info.getUrl())); 161 content.add(Arrays.asList(tr("Id"), info.getId() == null ? "-" : info.getId())); 162 if (info.getMinZoom() != 0) { 163 content.add(Arrays.asList(tr("Min. zoom"), Integer.toString(info.getMinZoom()))); 164 } 165 if (info.getMaxZoom() != 0) { 166 content.add(Arrays.asList(tr("Max. zoom"), Integer.toString(info.getMaxZoom()))); 167 } 168 if (info.getDescription() != null) { 169 content.add(Arrays.asList(tr("Description"), info.getDescription())); 170 } 171 for (List<String> entry: content) { 172 panel.add(new JLabel(entry.get(0) + ':'), GBC.std()); 173 panel.add(GBC.glue(5, 0), GBC.std()); 174 panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL)); 175 } 176 } 177 return panel; 178 } 179 180 protected JComponent createTextField(String text) { 181 if (text != null && text.matches("https?://.*")) { 182 return new UrlLabel(text); 183 } 184 JTextField ret = new JTextField(text); 185 ret.setEditable(false); 186 ret.setBorder(BorderFactory.createEmptyBorder()); 187 return ret; 188 } 189 190 /** 191 * Create a new imagery layer 192 * @param info The imagery info to use as base 193 * @return The created layer 194 */ 195 public static ImageryLayer create(ImageryInfo info) { 196 switch(info.getImageryType()) { 197 case WMS: 198 return new WMSLayer(info); 199 case WMTS: 200 return new WMTSLayer(info); 201 case TMS: 202 case BING: 203 case SCANEX: 204 return new TMSLayer(info); 205 default: 206 throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType())); 207 } 208 } 209 210 private static class ApplyOffsetAction extends AbstractAction { 211 private final transient OffsetMenuEntry menuEntry; 212 213 ApplyOffsetAction(OffsetMenuEntry menuEntry) { 214 super(menuEntry.getLabel()); 215 this.menuEntry = menuEntry; 216 } 217 218 @Override 219 public void actionPerformed(ActionEvent ev) { 220 menuEntry.actionPerformed(); 221 //TODO: Use some form of listeners for this. 222 MainApplication.getMenu().imageryMenu.refreshOffsetMenu(); 223 } 224 } 225 226 public class OffsetAction extends AbstractAction implements LayerAction { 227 @Override 228 public void actionPerformed(ActionEvent e) { 229 // Do nothing 230 } 231 232 @Override 233 public Component createMenuComponent() { 234 return getOffsetMenuItem(); 235 } 236 237 @Override 238 public boolean supportLayers(List<Layer> layers) { 239 return false; 240 } 241 } 242 243 /** 244 * Create the menu item that should be added to the offset menu. 245 * It may have a sub menu of e.g. bookmarks added to it. 246 * @return The menu item to add to the imagery menu. 247 */ 248 public JMenuItem getOffsetMenuItem() { 249 JMenu subMenu = new JMenu(trc("layer", "Offset")); 250 subMenu.setIcon(ImageProvider.get("mapmode", "adjustimg")); 251 return (JMenuItem) getOffsetMenuItem(subMenu); 252 } 253 254 /** 255 * Create the submenu or the menu item to set the offset of the layer. 256 * 257 * If only one menu item for this layer exists, it is returned by this method. 258 * 259 * If there are multiple, this method appends them to the subMenu and then returns the reference to the subMenu. 260 * @param subMenu The subMenu to use 261 * @return A single menu item to adjust the layer or the passed subMenu to which the menu items were appended. 262 */ 263 public JComponent getOffsetMenuItem(JComponent subMenu) { 264 JMenuItem adjustMenuItem = new JMenuItem(getAdjustAction()); 265 List<OffsetMenuEntry> usableBookmarks = getOffsetMenuEntries(); 266 if (usableBookmarks.isEmpty()) { 267 return adjustMenuItem; 268 } 269 270 subMenu.add(adjustMenuItem); 271 subMenu.add(new JSeparator()); 272 int menuItemHeight = 0; 273 for (OffsetMenuEntry b : usableBookmarks) { 274 JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b)); 275 item.setSelected(b.isActive()); 276 subMenu.add(item); 277 menuItemHeight = item.getPreferredSize().height; 278 } 279 if (menuItemHeight > 0) { 280 if (subMenu instanceof JMenu) { 281 MenuScroller.setScrollerFor((JMenu) subMenu); 282 } else if (subMenu instanceof JPopupMenu) { 283 MenuScroller.setScrollerFor((JPopupMenu) subMenu); 284 } 285 } 286 return subMenu; 287 } 288 289 protected abstract Action getAdjustAction(); 290 291 protected abstract List<OffsetMenuEntry> getOffsetMenuEntries(); 292 293 /** 294 * Gets the settings for the filter that is applied to this layer. 295 * @return The filter settings. 296 * @since 10547 297 */ 298 public ImageryFilterSettings getFilterSettings() { 299 return filterSettings; 300 } 301 302 /** 303 * This method adds the {@link ImageProcessor} to this Layer if it is not {@code null}. 304 * 305 * @param processor that processes the image 306 * 307 * @return true if processor was added, false otherwise 308 */ 309 public boolean addImageProcessor(ImageProcessor processor) { 310 return processor != null && imageProcessors.add(processor); 311 } 312 313 /** 314 * This method removes given {@link ImageProcessor} from this layer 315 * 316 * @param processor which is needed to be removed 317 * 318 * @return true if processor was removed 319 */ 320 public boolean removeImageProcessor(ImageProcessor processor) { 321 return imageProcessors.remove(processor); 322 } 323 324 /** 325 * Wraps a {@link BufferedImageOp} to be used as {@link ImageProcessor}. 326 * @param op the {@link BufferedImageOp} 327 * @param inPlace true to apply filter in place, i.e., not create a new {@link BufferedImage} for the result 328 * (the {@code op} needs to support this!) 329 * @return the {@link ImageProcessor} wrapper 330 */ 331 public static ImageProcessor createImageProcessor(final BufferedImageOp op, final boolean inPlace) { 332 return image -> op.filter(image, inPlace ? image : null); 333 } 334 335 /** 336 * This method gets all {@link ImageProcessor}s of the layer 337 * 338 * @return list of image processors without removed one 339 */ 340 public List<ImageProcessor> getImageProcessors() { 341 return imageProcessors; 342 } 343 344 /** 345 * Applies all the chosen {@link ImageProcessor}s to the image 346 * 347 * @param img - image which should be changed 348 * 349 * @return the new changed image 350 */ 351 public BufferedImage applyImageProcessors(BufferedImage img) { 352 for (ImageProcessor processor : imageProcessors) { 353 img = processor.process(img); 354 } 355 return img; 356 } 357 358 /** 359 * An additional menu entry in the imagery offset menu. 360 * @author Michael Zangl 361 * @see ImageryLayer#getOffsetMenuEntries() 362 * @since 13243 363 */ 364 public interface OffsetMenuEntry { 365 /** 366 * Get the label to use for this menu item 367 * @return The label to display in the menu. 368 */ 369 String getLabel(); 370 371 /** 372 * Test whether this bookmark is currently active 373 * @return <code>true</code> if it is active 374 */ 375 boolean isActive(); 376 377 /** 378 * Load this bookmark 379 */ 380 void actionPerformed(); 381 } 382 383 @Override 384 public String toString() { 385 return getClass().getSimpleName() + " [info=" + info + ']'; 386 } 387}