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.awt.image.ImagingOpException; 013import java.util.ArrayList; 014import java.util.Arrays; 015import java.util.List; 016import java.util.Locale; 017 018import javax.swing.AbstractAction; 019import javax.swing.Action; 020import javax.swing.BorderFactory; 021import javax.swing.Icon; 022import javax.swing.JCheckBoxMenuItem; 023import javax.swing.JComponent; 024import javax.swing.JLabel; 025import javax.swing.JMenu; 026import javax.swing.JMenuItem; 027import javax.swing.JPanel; 028import javax.swing.JPopupMenu; 029import javax.swing.JSeparator; 030import javax.swing.JTextField; 031 032import org.openstreetmap.josm.data.ProjectionBounds; 033import org.openstreetmap.josm.data.imagery.ImageryInfo; 034import org.openstreetmap.josm.data.preferences.IntegerProperty; 035import org.openstreetmap.josm.data.projection.ProjectionRegistry; 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.widgets.UrlLabel; 041import org.openstreetmap.josm.tools.GBC; 042import org.openstreetmap.josm.tools.ImageProcessor; 043import org.openstreetmap.josm.tools.ImageProvider; 044import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; 045import org.openstreetmap.josm.tools.Logging; 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 ProjectionRegistry.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 * Returns imagery info. 097 * @return imagery info 098 */ 099 public ImageryInfo getInfo() { 100 return info; 101 } 102 103 @Override 104 public Icon getIcon() { 105 return icon; 106 } 107 108 @Override 109 public boolean isMergable(Layer other) { 110 return false; 111 } 112 113 @Override 114 public void mergeFrom(Layer from) { 115 } 116 117 @Override 118 public Object getInfoComponent() { 119 JPanel panel = new JPanel(new GridBagLayout()); 120 panel.add(new JLabel(getToolTipText()), GBC.eol()); 121 if (info != null) { 122 List<List<String>> content = new ArrayList<>(); 123 content.add(Arrays.asList(tr("Name"), info.getName())); 124 content.add(Arrays.asList(tr("Type"), info.getImageryType().getTypeString().toUpperCase(Locale.ENGLISH))); 125 content.add(Arrays.asList(tr("URL"), info.getUrl())); 126 content.add(Arrays.asList(tr("Id"), info.getId() == null ? "-" : info.getId())); 127 if (info.getMinZoom() != 0) { 128 content.add(Arrays.asList(tr("Min. zoom"), Integer.toString(info.getMinZoom()))); 129 } 130 if (info.getMaxZoom() != 0) { 131 content.add(Arrays.asList(tr("Max. zoom"), Integer.toString(info.getMaxZoom()))); 132 } 133 if (info.getDescription() != null) { 134 content.add(Arrays.asList(tr("Description"), info.getDescription())); 135 } 136 for (List<String> entry: content) { 137 panel.add(new JLabel(entry.get(0) + ':'), GBC.std()); 138 panel.add(GBC.glue(5, 0), GBC.std()); 139 panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL)); 140 } 141 } 142 return panel; 143 } 144 145 protected JComponent createTextField(String text) { 146 if (text != null && text.matches("https?://.*")) { 147 return new UrlLabel(text); 148 } 149 JTextField ret = new JTextField(text); 150 ret.setEditable(false); 151 ret.setBorder(BorderFactory.createEmptyBorder()); 152 return ret; 153 } 154 155 /** 156 * Create a new imagery layer 157 * @param info The imagery info to use as base 158 * @return The created layer 159 */ 160 public static ImageryLayer create(ImageryInfo info) { 161 switch(info.getImageryType()) { 162 case WMS: 163 case WMS_ENDPOINT: 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 private static class ApplyOffsetAction extends AbstractAction { 177 private final transient OffsetMenuEntry menuEntry; 178 179 ApplyOffsetAction(OffsetMenuEntry menuEntry) { 180 super(menuEntry.getLabel()); 181 this.menuEntry = menuEntry; 182 } 183 184 @Override 185 public void actionPerformed(ActionEvent ev) { 186 menuEntry.actionPerformed(); 187 //TODO: Use some form of listeners for this. 188 MainApplication.getMenu().imageryMenu.refreshOffsetMenu(); 189 } 190 } 191 192 public class OffsetAction extends AbstractAction implements LayerAction { 193 @Override 194 public void actionPerformed(ActionEvent e) { 195 // Do nothing 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 /** 210 * Create the menu item that should be added to the offset menu. 211 * It may have a sub menu of e.g. bookmarks added to it. 212 * @return The menu item to add to the imagery menu. 213 */ 214 public JMenuItem getOffsetMenuItem() { 215 JMenu subMenu = new JMenu(trc("layer", "Offset")); 216 subMenu.setIcon(ImageProvider.get("mapmode", "adjustimg")); 217 return (JMenuItem) getOffsetMenuItem(subMenu); 218 } 219 220 /** 221 * Create the submenu or the menu item to set the offset of the layer. 222 * 223 * If only one menu item for this layer exists, it is returned by this method. 224 * 225 * If there are multiple, this method appends them to the subMenu and then returns the reference to the subMenu. 226 * @param subMenu The subMenu to use 227 * @return A single menu item to adjust the layer or the passed subMenu to which the menu items were appended. 228 */ 229 public JComponent getOffsetMenuItem(JComponent subMenu) { 230 JMenuItem adjustMenuItem = new JMenuItem(getAdjustAction()); 231 List<OffsetMenuEntry> usableBookmarks = getOffsetMenuEntries(); 232 if (usableBookmarks.isEmpty()) { 233 return adjustMenuItem; 234 } 235 236 subMenu.add(adjustMenuItem); 237 subMenu.add(new JSeparator()); 238 int menuItemHeight = 0; 239 for (OffsetMenuEntry b : usableBookmarks) { 240 JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b)); 241 item.setSelected(b.isActive()); 242 subMenu.add(item); 243 menuItemHeight = item.getPreferredSize().height; 244 } 245 if (menuItemHeight > 0) { 246 if (subMenu instanceof JMenu) { 247 MenuScroller.setScrollerFor((JMenu) subMenu); 248 } else if (subMenu instanceof JPopupMenu) { 249 MenuScroller.setScrollerFor((JPopupMenu) subMenu); 250 } 251 } 252 return subMenu; 253 } 254 255 protected abstract Action getAdjustAction(); 256 257 protected abstract List<OffsetMenuEntry> getOffsetMenuEntries(); 258 259 /** 260 * Gets the settings for the filter that is applied to this layer. 261 * @return The filter settings. 262 * @since 10547 263 */ 264 public ImageryFilterSettings getFilterSettings() { 265 return filterSettings; 266 } 267 268 /** 269 * This method adds the {@link ImageProcessor} to this Layer if it is not {@code null}. 270 * 271 * @param processor that processes the image 272 * 273 * @return true if processor was added, false otherwise 274 */ 275 public boolean addImageProcessor(ImageProcessor processor) { 276 return processor != null && imageProcessors.add(processor); 277 } 278 279 /** 280 * This method removes given {@link ImageProcessor} from this layer 281 * 282 * @param processor which is needed to be removed 283 * 284 * @return true if processor was removed 285 */ 286 public boolean removeImageProcessor(ImageProcessor processor) { 287 return imageProcessors.remove(processor); 288 } 289 290 /** 291 * Wraps a {@link BufferedImageOp} to be used as {@link ImageProcessor}. 292 * @param op the {@link BufferedImageOp} 293 * @param inPlace true to apply filter in place, i.e., not create a new {@link BufferedImage} for the result 294 * (the {@code op} needs to support this!) 295 * @return the {@link ImageProcessor} wrapper 296 */ 297 public static ImageProcessor createImageProcessor(final BufferedImageOp op, final boolean inPlace) { 298 return image -> op.filter(image, inPlace ? image : null); 299 } 300 301 /** 302 * This method gets all {@link ImageProcessor}s of the layer 303 * 304 * @return list of image processors without removed one 305 */ 306 public List<ImageProcessor> getImageProcessors() { 307 return imageProcessors; 308 } 309 310 /** 311 * Applies all the chosen {@link ImageProcessor}s to the image 312 * 313 * @param img - image which should be changed 314 * 315 * @return the new changed image 316 */ 317 public BufferedImage applyImageProcessors(BufferedImage img) { 318 for (ImageProcessor processor : imageProcessors) { 319 try { 320 img = processor.process(img); 321 } catch (ImagingOpException e) { 322 Logging.error(e); 323 } 324 } 325 return img; 326 } 327 328 /** 329 * An additional menu entry in the imagery offset menu. 330 * @author Michael Zangl 331 * @see ImageryLayer#getOffsetMenuEntries() 332 * @since 13243 333 */ 334 public interface OffsetMenuEntry { 335 /** 336 * Get the label to use for this menu item 337 * @return The label to display in the menu. 338 */ 339 String getLabel(); 340 341 /** 342 * Test whether this bookmark is currently active 343 * @return <code>true</code> if it is active 344 */ 345 boolean isActive(); 346 347 /** 348 * Load this bookmark 349 */ 350 void actionPerformed(); 351 } 352 353 @Override 354 public String toString() { 355 return getClass().getSimpleName() + " [info=" + info + ']'; 356 } 357}