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