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.Collection;
010import java.util.List;
011import java.util.Objects;
012
013import javax.swing.AbstractAction;
014import javax.swing.Action;
015
016import org.apache.commons.jcs.access.CacheAccess;
017import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
018import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
019import org.openstreetmap.josm.data.coor.LatLon;
020import org.openstreetmap.josm.data.imagery.AbstractWMSTileSource;
021import org.openstreetmap.josm.data.imagery.ImageryInfo;
022import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
023import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
024import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource;
025import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader;
026import org.openstreetmap.josm.data.imagery.WMSEndpointTileSource;
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.data.projection.ProjectionRegistry;
031import org.openstreetmap.josm.data.projection.Projections;
032import org.openstreetmap.josm.gui.MainApplication;
033import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
034import org.openstreetmap.josm.tools.CheckParameterUtil;
035import org.openstreetmap.josm.tools.Logging;
036import org.openstreetmap.josm.tools.Utils;
037
038/**
039 * This is a layer that grabs the current screen from an WMS server. The data
040 * fetched this way is tiled and managed to the disc to reduce server load.
041 *
042 */
043public class WMSLayer extends AbstractCachedTileSourceLayer<AbstractWMSTileSource> {
044    private static final String PREFERENCE_PREFIX = "imagery.wms";
045    /**
046     * Registers all setting properties
047     */
048    static {
049        new TileSourceDisplaySettings(PREFERENCE_PREFIX);
050    }
051
052    /** default tile size for WMS Layer */
053    public static final IntegerProperty PROP_IMAGE_SIZE = new IntegerProperty(PREFERENCE_PREFIX + ".imageSize", 512);
054
055    /** should WMS layer autozoom in default mode */
056    public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true);
057
058    private static final String CACHE_REGION_NAME = "WMS";
059
060    private List<String> serverProjections;
061
062    /**
063     * Constructs a new {@code WMSLayer}.
064     * @param info ImageryInfo description of the layer
065     */
066    public WMSLayer(ImageryInfo info) {
067        super(info);
068        CheckParameterUtil.ensureThat(
069                info.getImageryType() == ImageryType.WMS || info.getImageryType() == ImageryType.WMS_ENDPOINT, "ImageryType is WMS");
070        CheckParameterUtil.ensureParameterNotNull(info.getUrl(), "info.url");
071        if (info.getImageryType() == ImageryType.WMS) {
072            TemplatedWMSTileSource.checkUrl(info.getUrl());
073
074        }
075        this.serverProjections = new ArrayList<>(info.getServerProjections());
076    }
077
078    @Override
079    protected TileSourceDisplaySettings createDisplaySettings() {
080        return new TileSourceDisplaySettings(PREFERENCE_PREFIX);
081    }
082
083    @Override
084    public Action[] getMenuEntries() {
085        List<Action> ret = new ArrayList<>();
086        ret.addAll(Arrays.asList(super.getMenuEntries()));
087        ret.add(SeparatorLayerAction.INSTANCE);
088        ret.add(new LayerSaveAction(this));
089        ret.add(new LayerSaveAsAction(this));
090        ret.add(new BookmarkWmsAction());
091        return ret.toArray(new Action[0]);
092    }
093
094    @Override
095    protected AbstractWMSTileSource getTileSource() {
096        AbstractWMSTileSource tileSource;
097        if (info.getImageryType() == ImageryType.WMS) {
098            tileSource = new TemplatedWMSTileSource(info, chooseProjection(ProjectionRegistry.getProjection()));
099        } else {
100            /*
101             *  Chicken-and-egg problem. We want to create tile source, but supported projections we can get only
102             *  from this tile source. So create tilesource first with dummy ProjectionRegistry.getProjection(), and then update
103             *  once we update server projections.
104             *
105             *  Thus:
106             *  * it is not required to provide projections for wms_endpoint imagery types
107             *  * we always use current definitions returned by server
108             */
109            WMSEndpointTileSource endpointTileSource = new WMSEndpointTileSource(info, ProjectionRegistry.getProjection());
110            this.serverProjections = endpointTileSource.getServerProjections();
111            endpointTileSource.setTileProjection(chooseProjection(ProjectionRegistry.getProjection()));
112            tileSource = endpointTileSource;
113        }
114        info.setAttribution(tileSource);
115        return tileSource;
116    }
117
118    /**
119     * This action will add a WMS layer menu entry with the current WMS layer
120     * URL and name extended by the current resolution.
121     * When using the menu entry again, the WMS cache will be used properly.
122     */
123    public class BookmarkWmsAction extends AbstractAction {
124        /**
125         * Constructs a new {@code BookmarkWmsAction}.
126         */
127        public BookmarkWmsAction() {
128            super(tr("Set WMS Bookmark"));
129        }
130
131        @Override
132        public void actionPerformed(ActionEvent ev) {
133            ImageryLayerInfo.addLayer(new ImageryInfo(info));
134        }
135    }
136
137    @Override
138    public Collection<String> getNativeProjections() {
139        return serverProjections;
140    }
141
142    @Override
143    public void projectionChanged(Projection oldValue, Projection newValue) {
144        super.projectionChanged(oldValue, newValue);
145        Projection tileProjection = chooseProjection(newValue);
146        if (!Objects.equals(tileSource.getTileProjection(), tileProjection)) {
147            tileSource.setTileProjection(tileProjection);
148        }
149    }
150
151    private Projection chooseProjection(Projection requested) {
152        if (serverProjections.contains(requested.toCode())) {
153            return requested;
154        } else {
155            LatLon center = MainApplication.isDisplayingMapView() ?
156                    requested.eastNorth2latlon(MainApplication.getMap().mapView.getCenter()) : null;
157            Projection firstNonNullproj = null;
158            Projection firstProjInBounds = null;
159            for (String code : serverProjections) {
160                Projection proj = Projections.getProjectionByCode(code);
161                if (proj != null) {
162                    if (firstNonNullproj == null) {
163                        firstNonNullproj = proj;
164                    }
165                    if (center != null && proj.getWorldBoundsLatLon().contains(center)) {
166                        firstProjInBounds = proj;
167                        break;
168                    }
169                }
170            }
171            if (firstProjInBounds != null) {
172                return selectProjection(firstProjInBounds);
173            } else if (firstNonNullproj != null) {
174                return selectProjection(firstNonNullproj);
175            }
176            Logging.warn(tr("Unable to find supported projection for layer {0}. Using {1}.", getName(), requested.toCode()));
177            return requested;
178        }
179    }
180
181    private Projection selectProjection(Projection proj) {
182        Logging.info(tr("Reprojecting layer {0} from {1} to {2}. For best image quality and performance,"
183                + " switch to one of the supported projections: {3}",
184                getName(), proj.toCode(), ProjectionRegistry.getProjection().toCode(), Utils.join(", ", getNativeProjections())));
185        return proj;
186    }
187
188    @Override
189    protected Class<? extends TileLoader> getTileLoaderClass() {
190        return WMSCachedTileLoader.class;
191    }
192
193    @Override
194    protected String getCacheName() {
195        return CACHE_REGION_NAME;
196    }
197
198    /**
199     * @return cache region for WMS layer
200     */
201    public static CacheAccess<String, BufferedImageCacheEntry> getCache() {
202        return AbstractCachedTileSourceLayer.getCache(CACHE_REGION_NAME);
203    }
204}