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