001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.event.ActionEvent;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.List;
010import java.util.Set;
011import java.util.TreeSet;
012
013import javax.swing.AbstractAction;
014import javax.swing.Action;
015import javax.swing.JOptionPane;
016
017import org.apache.commons.jcs.access.CacheAccess;
018import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
019import org.openstreetmap.josm.Main;
020import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
021import org.openstreetmap.josm.data.imagery.AbstractWMSTileSource;
022import org.openstreetmap.josm.data.imagery.ImageryInfo;
023import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
024import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
025import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource;
026import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader;
027import org.openstreetmap.josm.data.preferences.BooleanProperty;
028import org.openstreetmap.josm.data.preferences.IntegerProperty;
029import org.openstreetmap.josm.data.projection.Projection;
030import org.openstreetmap.josm.gui.ExtendedDialog;
031import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
032import org.openstreetmap.josm.tools.CheckParameterUtil;
033
034/**
035 * This is a layer that grabs the current screen from an WMS server. The data
036 * fetched this way is tiled and managed to the disc to reduce server load.
037 *
038 */
039public class WMSLayer extends AbstractCachedTileSourceLayer<AbstractWMSTileSource> {
040    private static final String PREFERENCE_PREFIX = "imagery.wms";
041    /**
042     * Registers all setting properties
043     */
044    static {
045        new TileSourceDisplaySettings(PREFERENCE_PREFIX);
046    }
047
048    /** default tile size for WMS Layer */
049    public static final IntegerProperty PROP_IMAGE_SIZE = new IntegerProperty(PREFERENCE_PREFIX + ".imageSize", 512);
050
051    /** should WMS layer autozoom in default mode */
052    public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true);
053
054    private static final String CACHE_REGION_NAME = "WMS";
055
056    private final Set<String> supportedProjections;
057
058    /**
059     * Constructs a new {@code WMSLayer}.
060     * @param info ImageryInfo description of the layer
061     */
062    public WMSLayer(ImageryInfo info) {
063        super(info);
064        CheckParameterUtil.ensureThat(info.getImageryType() == ImageryType.WMS, "ImageryType is WMS");
065        CheckParameterUtil.ensureParameterNotNull(info.getUrl(), "info.url");
066        TemplatedWMSTileSource.checkUrl(info.getUrl());
067        this.supportedProjections = new TreeSet<>(info.getServerProjections());
068    }
069
070    @Override
071    protected TileSourceDisplaySettings createDisplaySettings() {
072        return new TileSourceDisplaySettings(PREFERENCE_PREFIX);
073    }
074
075    @Override
076    public Action[] getMenuEntries() {
077        List<Action> ret = new ArrayList<>();
078        ret.addAll(Arrays.asList(super.getMenuEntries()));
079        ret.add(SeparatorLayerAction.INSTANCE);
080        ret.add(new LayerSaveAction(this));
081        ret.add(new LayerSaveAsAction(this));
082        ret.add(new BookmarkWmsAction());
083        return ret.toArray(new Action[ret.size()]);
084    }
085
086    @Override
087    protected AbstractWMSTileSource getTileSource() {
088        AbstractWMSTileSource tileSource = new TemplatedWMSTileSource(info);
089        info.setAttribution(tileSource);
090        return tileSource;
091    }
092
093    /**
094     * This action will add a WMS layer menu entry with the current WMS layer
095     * URL and name extended by the current resolution.
096     * When using the menu entry again, the WMS cache will be used properly.
097     */
098    public class BookmarkWmsAction extends AbstractAction {
099        /**
100         * Constructs a new {@code BookmarkWmsAction}.
101         */
102        public BookmarkWmsAction() {
103            super(tr("Set WMS Bookmark"));
104        }
105
106        @Override
107        public void actionPerformed(ActionEvent ev) {
108            ImageryLayerInfo.addLayer(new ImageryInfo(info));
109        }
110    }
111
112    @Override
113    public boolean isProjectionSupported(Projection proj) {
114        return supportedProjections == null || supportedProjections.isEmpty() || supportedProjections.contains(proj.toCode()) ||
115                (info.isEpsg4326To3857Supported() && supportedProjections.contains("EPSG:4326")
116                        && "EPSG:3857".equals(Main.getProjection().toCode()));
117    }
118
119    @Override
120    public String nameSupportedProjections() {
121        StringBuilder ret = new StringBuilder();
122        for (String e: supportedProjections) {
123            ret.append(e).append(", ");
124        }
125        String appendix = "";
126
127        if (isReprojectionPossible()) {
128            appendix = ". <p>" + tr("JOSM will use EPSG:4326 to query the server, but results may vary "
129                    + "depending on the WMS server") + "</p>";
130        }
131        return ret.substring(0, ret.length()-2) + appendix;
132    }
133
134    @Override
135    public void projectionChanged(Projection oldValue, Projection newValue) {
136        // do not call super - we need custom warning dialog
137
138        if (!isProjectionSupported(newValue)) {
139            String message =
140                    "<html><body><p>" + tr("The layer {0} does not support the new projection {1}.", getName(), newValue.toCode()) +
141                    "<p style='width: 450px; position: absolute; margin: 0px;'>" +
142                            tr("Supported projections are: {0}", nameSupportedProjections()) + "</p>" +
143                    "<p>" + tr("Change the projection again or remove the layer.");
144
145            ExtendedDialog warningDialog = new ExtendedDialog(Main.parent, tr("Warning"), new String[]{tr("OK")}).
146                    setContent(message).
147                    setIcon(JOptionPane.WARNING_MESSAGE);
148
149            if (isReprojectionPossible()) {
150                warningDialog.toggleEnable("imagery.wms.projectionSupportWarnings." + tileSource.getBaseUrl());
151            }
152            warningDialog.showDialog();
153        }
154
155        if (!newValue.equals(oldValue)) {
156            tileSource.initProjection(newValue);
157        }
158    }
159
160    @Override
161    protected Class<? extends TileLoader> getTileLoaderClass() {
162        return WMSCachedTileLoader.class;
163    }
164
165    @Override
166    protected String getCacheName() {
167        return CACHE_REGION_NAME;
168    }
169
170    /**
171     * @return cache region for WMS layer
172     */
173    public static CacheAccess<String, BufferedImageCacheEntry> getCache() {
174        return AbstractCachedTileSourceLayer.getCache(CACHE_REGION_NAME);
175    }
176
177    private boolean isReprojectionPossible() {
178        return supportedProjections.contains("EPSG:4326") && "EPSG:3857".equals(Main.getProjection().toCode());
179    }
180}