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.layer.imagery.MVTLayer; 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; 046import org.openstreetmap.josm.tools.Logging; 047 048/** 049 * Abstract base class for background imagery layers ({@link WMSLayer}, {@link TMSLayer}, {@link WMTSLayer}). 050 * 051 * Handles some common tasks, like image filters, image processors, etc. 052 */ 053public abstract class ImageryLayer extends Layer { 054 055 /** 056 * The default value for the sharpen filter for each imagery layer. 057 */ 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 protected final ImageryInfo info; 063 064 protected Icon icon; 065 066 private final ImageryFilterSettings filterSettings = new ImageryFilterSettings(); 067 068 /** 069 * Constructs a new {@code ImageryLayer}. 070 * @param info imagery info 071 */ 072 protected ImageryLayer(ImageryInfo info) { 073 super(info.getName()); 074 this.info = info; 075 if (info.getIcon() != null) { 076 icon = new ImageProvider(info.getIcon()).setOptional(true). 077 setMaxSize(ImageSizes.LAYER).get(); 078 } 079 if (icon == null) { 080 icon = ImageProvider.get("imagery_menu", ImageSizes.LAYER); 081 } 082 for (ImageProcessor processor : filterSettings.getProcessors()) { 083 addImageProcessor(processor); 084 } 085 filterSettings.setSharpenLevel(1 + PROP_SHARPEN_LEVEL.get() / 2f); 086 } 087 088 public double getPPD() { 089 if (!MainApplication.isDisplayingMapView()) 090 return ProjectionRegistry.getProjection().getDefaultZoomInPPD(); 091 MapView mapView = MainApplication.getMap().mapView; 092 ProjectionBounds bounds = mapView.getProjectionBounds(); 093 return mapView.getWidth() / (bounds.maxEast - bounds.minEast); 094 } 095 096 /** 097 * Returns imagery info. 098 * @return imagery info 099 */ 100 public ImageryInfo getInfo() { 101 return info; 102 } 103 104 @Override 105 public Icon getIcon() { 106 return icon; 107 } 108 109 @Override 110 public boolean isMergable(Layer other) { 111 return false; 112 } 113 114 @Override 115 public void mergeFrom(Layer from) { 116 } 117 118 @Override 119 public Object getInfoComponent() { 120 JPanel panel = new JPanel(new GridBagLayout()); 121 panel.add(new JLabel(getToolTipText()), GBC.eol()); 122 if (info != null) { 123 List<List<String>> content = new ArrayList<>(); 124 content.add(Arrays.asList(tr("Name"), info.getName())); 125 content.add(Arrays.asList(tr("Type"), info.getImageryType().getTypeString().toUpperCase(Locale.ENGLISH))); 126 content.add(Arrays.asList(tr("URL"), info.getUrl())); 127 content.add(Arrays.asList(tr("Id"), info.getId() == null ? "-" : info.getId())); 128 if (info.getMinZoom() != 0) { 129 content.add(Arrays.asList(tr("Min. zoom"), Integer.toString(info.getMinZoom()))); 130 } 131 if (info.getMaxZoom() != 0) { 132 content.add(Arrays.asList(tr("Max. zoom"), Integer.toString(info.getMaxZoom()))); 133 } 134 if (info.getDescription() != null) { 135 content.add(Arrays.asList(tr("Description"), info.getDescription())); 136 } 137 for (List<String> entry: content) { 138 panel.add(new JLabel(entry.get(0) + ':'), GBC.std()); 139 panel.add(GBC.glue(5, 0), GBC.std()); 140 panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL)); 141 } 142 } 143 return panel; 144 } 145 146 protected JComponent createTextField(String text) { 147 if (text != null && text.matches("https?://.*")) { 148 return new UrlLabel(text); 149 } 150 JTextField ret = new JTextField(text); 151 ret.setEditable(false); 152 ret.setBorder(BorderFactory.createEmptyBorder()); 153 return ret; 154 } 155 156 /** 157 * Create a new imagery layer 158 * @param info The imagery info to use as base 159 * @return The created layer 160 */ 161 public static ImageryLayer create(ImageryInfo info) { 162 switch(info.getImageryType()) { 163 case WMS: 164 case WMS_ENDPOINT: 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 case MVT: 173 return new MVTLayer(info); 174 default: 175 throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType())); 176 } 177 } 178 179 private static class ApplyOffsetAction extends AbstractAction { 180 private final transient OffsetMenuEntry menuEntry; 181 182 ApplyOffsetAction(OffsetMenuEntry menuEntry) { 183 super(menuEntry.getLabel()); 184 this.menuEntry = menuEntry; 185 } 186 187 @Override 188 public void actionPerformed(ActionEvent ev) { 189 menuEntry.actionPerformed(); 190 //TODO: Use some form of listeners for this. 191 MainApplication.getMenu().imageryMenu.refreshOffsetMenu(); 192 } 193 } 194 195 public class OffsetAction extends AbstractAction implements LayerAction { 196 @Override 197 public void actionPerformed(ActionEvent e) { 198 // Do nothing 199 } 200 201 @Override 202 public Component createMenuComponent() { 203 return getOffsetMenuItem(); 204 } 205 206 @Override 207 public boolean supportLayers(List<Layer> layers) { 208 return false; 209 } 210 } 211 212 /** 213 * Create the menu item that should be added to the offset menu. 214 * It may have a sub menu of e.g. bookmarks added to it. 215 * @return The menu item to add to the imagery menu. 216 */ 217 public JMenuItem getOffsetMenuItem() { 218 JMenu subMenu = new JMenu(trc("layer", "Offset")); 219 subMenu.setIcon(ImageProvider.get("mapmode", "adjustimg")); 220 return (JMenuItem) getOffsetMenuItem(subMenu); 221 } 222 223 /** 224 * Create the submenu or the menu item to set the offset of the layer. 225 * 226 * If only one menu item for this layer exists, it is returned by this method. 227 * 228 * If there are multiple, this method appends them to the subMenu and then returns the reference to the subMenu. 229 * @param subMenu The subMenu to use 230 * @return A single menu item to adjust the layer or the passed subMenu to which the menu items were appended. 231 */ 232 public JComponent getOffsetMenuItem(JComponent subMenu) { 233 JMenuItem adjustMenuItem = new JMenuItem(getAdjustAction()); 234 List<OffsetMenuEntry> usableBookmarks = getOffsetMenuEntries(); 235 if (usableBookmarks.isEmpty()) { 236 return adjustMenuItem; 237 } 238 239 subMenu.add(adjustMenuItem); 240 subMenu.add(new JSeparator()); 241 int menuItemHeight = 0; 242 for (OffsetMenuEntry b : usableBookmarks) { 243 JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b)); 244 item.setSelected(b.isActive()); 245 subMenu.add(item); 246 menuItemHeight = item.getPreferredSize().height; 247 } 248 if (menuItemHeight > 0) { 249 if (subMenu instanceof JMenu) { 250 MenuScroller.setScrollerFor((JMenu) subMenu); 251 } else if (subMenu instanceof JPopupMenu) { 252 MenuScroller.setScrollerFor((JPopupMenu) subMenu); 253 } 254 } 255 return subMenu; 256 } 257 258 protected abstract Action getAdjustAction(); 259 260 protected abstract List<OffsetMenuEntry> getOffsetMenuEntries(); 261 262 /** 263 * Gets the settings for the filter that is applied to this layer. 264 * @return The filter settings. 265 * @since 10547 266 */ 267 public ImageryFilterSettings getFilterSettings() { 268 return filterSettings; 269 } 270 271 /** 272 * This method adds the {@link ImageProcessor} to this Layer if it is not {@code null}. 273 * 274 * @param processor that processes the image 275 * 276 * @return true if processor was added, false otherwise 277 */ 278 public boolean addImageProcessor(ImageProcessor processor) { 279 return processor != null && imageProcessors.add(processor); 280 } 281 282 /** 283 * This method removes given {@link ImageProcessor} from this layer 284 * 285 * @param processor which is needed to be removed 286 * 287 * @return true if processor was removed 288 */ 289 public boolean removeImageProcessor(ImageProcessor processor) { 290 return imageProcessors.remove(processor); 291 } 292 293 /** 294 * Wraps a {@link BufferedImageOp} to be used as {@link ImageProcessor}. 295 * @param op the {@link BufferedImageOp} 296 * @param inPlace true to apply filter in place, i.e., not create a new {@link BufferedImage} for the result 297 * (the {@code op} needs to support this!) 298 * @return the {@link ImageProcessor} wrapper 299 */ 300 public static ImageProcessor createImageProcessor(final BufferedImageOp op, final boolean inPlace) { 301 return image -> op.filter(image, inPlace ? image : null); 302 } 303 304 /** 305 * This method gets all {@link ImageProcessor}s of the layer 306 * 307 * @return list of image processors without removed one 308 */ 309 public List<ImageProcessor> getImageProcessors() { 310 return imageProcessors; 311 } 312 313 /** 314 * Applies all the chosen {@link ImageProcessor}s to the image 315 * 316 * @param img - image which should be changed 317 * 318 * @return the new changed image 319 */ 320 public BufferedImage applyImageProcessors(BufferedImage img) { 321 for (ImageProcessor processor : imageProcessors) { 322 try { 323 img = processor.process(img); 324 } catch (ImagingOpException e) { 325 Logging.error(e); 326 } 327 } 328 return img; 329 } 330 331 /** 332 * An additional menu entry in the imagery offset menu. 333 * @author Michael Zangl 334 * @see ImageryLayer#getOffsetMenuEntries() 335 * @since 13243 336 */ 337 public interface OffsetMenuEntry { 338 /** 339 * Get the label to use for this menu item 340 * @return The label to display in the menu. 341 */ 342 String getLabel(); 343 344 /** 345 * Test whether this bookmark is currently active 346 * @return <code>true</code> if it is active 347 */ 348 boolean isActive(); 349 350 /** 351 * Load this bookmark 352 */ 353 void actionPerformed(); 354 } 355 356 @Override 357 public String toString() { 358 return getClass().getSimpleName() + " [info=" + info + ']'; 359 } 360 361 @Override 362 public String getChangesetSourceTag() { 363 return getInfo().getSourceName(); 364 } 365}