001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.GridBagConstraints;
008import java.awt.GridBagLayout;
009import java.awt.event.ActionEvent;
010import java.awt.event.KeyEvent;
011import java.io.IOException;
012import java.util.ArrayList;
013import java.util.List;
014import java.util.regex.Matcher;
015import java.util.regex.Pattern;
016
017import javax.swing.ButtonGroup;
018import javax.swing.JLabel;
019import javax.swing.JOptionPane;
020import javax.swing.JPanel;
021import javax.swing.JRadioButton;
022
023import org.openstreetmap.josm.Main;
024import org.openstreetmap.josm.data.imagery.ImageryInfo;
025import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
026import org.openstreetmap.josm.gui.ExtendedDialog;
027import org.openstreetmap.josm.gui.MainApplication;
028import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
029import org.openstreetmap.josm.gui.layer.ImageryLayer;
030import org.openstreetmap.josm.gui.widgets.JosmTextField;
031import org.openstreetmap.josm.gui.widgets.UrlLabel;
032import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException;
033import org.openstreetmap.josm.tools.GBC;
034import org.openstreetmap.josm.tools.Logging;
035import org.openstreetmap.josm.tools.Shortcut;
036
037/**
038 * Download rectified images from various services.
039 * @since 3715
040 */
041public class MapRectifierWMSmenuAction extends JosmAction {
042
043    /**
044     * Class that bundles all required information of a rectifier service
045     */
046    public static class RectifierService {
047        private final String name;
048        private final String url;
049        private final String wmsUrl;
050        private final Pattern urlRegEx;
051        private final Pattern idValidator;
052        private JRadioButton btn;
053
054        /**
055         * Constructs a new {@code RectifierService}.
056         * @param name Name of the rectifing service
057         * @param url URL to the service where users can register, upload, etc.
058         * @param wmsUrl URL to the WMS server where JOSM will grab the images. Insert __s__ where the ID should be placed
059         * @param urlRegEx a regular expression that determines if a given URL is one of the service and returns the WMS id if so
060         * @param idValidator regular expression that checks if a given ID is syntactically valid
061         */
062        public RectifierService(String name, String url, String wmsUrl, String urlRegEx, String idValidator) {
063            this.name = name;
064            this.url = url;
065            this.wmsUrl = wmsUrl;
066            this.urlRegEx = Pattern.compile(urlRegEx);
067            this.idValidator = Pattern.compile(idValidator);
068        }
069
070        private boolean isSelected() {
071            return btn.isSelected();
072        }
073    }
074
075    /**
076     * List of available rectifier services.
077     */
078    private final transient List<RectifierService> services = new ArrayList<>();
079
080    /**
081     * Constructs a new {@code MapRectifierWMSmenuAction}.
082     */
083    public MapRectifierWMSmenuAction() {
084        super(tr("Rectified Image..."),
085                "OLmarker",
086                tr("Download Rectified Images From Various Services"),
087                Shortcut.registerShortcut("imagery:rectimg",
088                        tr("Imagery: {0}", tr("Rectified Image...")),
089                        KeyEvent.CHAR_UNDEFINED, Shortcut.NONE),
090                true
091        );
092        putValue("help", ht("/Menu/Imagery"));
093
094        // Add default services
095        services.add(
096                new RectifierService("Metacarta Map Rectifier",
097                        "http://labs.metacarta.com/rectifier/",
098                        "http://labs.metacarta.com/rectifier/wms.cgi?id=__s__&srs=EPSG:4326"
099                        + "&Service=WMS&Version=1.1.0&Request=GetMap&format=image/png&",
100                        // This matches more than the "classic" WMS link, so users can pretty much
101                        // copy any link as long as it includes the ID
102                        "labs\\.metacarta\\.com/(?:.*?)(?:/|=)([0-9]+)(?:\\?|/|\\.|$)",
103                "^[0-9]+$")
104        );
105        services.add(
106                new RectifierService("Map Warper",
107                        "http://mapwarper.net/",
108                        "http://mapwarper.net/maps/wms/__s__?request=GetMap&version=1.1.1"
109                        + "&styles=&format=image/png&srs=epsg:4326&exceptions=application/vnd.ogc.se_inimage&",
110                        // This matches more than the "classic" WMS link, so users can pretty much
111                        // copy any link as long as it includes the ID
112                        "(?:mapwarper\\.net|warper\\.geothings\\.net)/(?:.*?)/([0-9]+)(?:\\?|/|\\.|$)",
113                "^[0-9]+$")
114        );
115
116        // This service serves the purpose of "just this once" without forcing the user
117        // to commit the link to the preferences
118
119        // Clipboard content gets trimmed, so matching whitespace only ensures that this
120        // service will never be selected automatically.
121        services.add(new RectifierService(tr("Custom WMS Link"), "", "", "^\\s+$", ""));
122    }
123
124    @Override
125    public void actionPerformed(ActionEvent e) {
126        if (!isEnabled()) return;
127        JPanel panel = new JPanel(new GridBagLayout());
128        panel.add(new JLabel(tr("Supported Rectifier Services:")), GBC.eol());
129
130        JosmTextField tfWmsUrl = new JosmTextField(30);
131
132        String clip = ClipboardUtils.getClipboardStringContent();
133        clip = clip == null ? "" : clip.trim();
134        ButtonGroup group = new ButtonGroup();
135
136        JRadioButton firstBtn = null;
137        for (RectifierService s : services) {
138            JRadioButton serviceBtn = new JRadioButton(s.name);
139            if (firstBtn == null) {
140                firstBtn = serviceBtn;
141            }
142            // Checks clipboard contents against current service if no match has been found yet.
143            // If the contents match, they will be inserted into the text field and the corresponding
144            // service will be pre-selected.
145            if (!clip.isEmpty() && tfWmsUrl.getText().isEmpty()
146                    && (s.urlRegEx.matcher(clip).find() || s.idValidator.matcher(clip).matches())) {
147                serviceBtn.setSelected(true);
148                tfWmsUrl.setText(clip);
149            }
150            s.btn = serviceBtn;
151            group.add(serviceBtn);
152            if (!s.url.isEmpty()) {
153                panel.add(serviceBtn, GBC.std());
154                panel.add(new UrlLabel(s.url, tr("Visit Homepage")), GBC.eol().anchor(GridBagConstraints.EAST));
155            } else {
156                panel.add(serviceBtn, GBC.eol().anchor(GridBagConstraints.WEST));
157            }
158        }
159
160        // Fallback in case no match was found
161        if (tfWmsUrl.getText().isEmpty() && firstBtn != null) {
162            firstBtn.setSelected(true);
163        }
164
165        panel.add(new JLabel(tr("WMS URL or Image ID:")), GBC.eol());
166        panel.add(tfWmsUrl, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
167
168        ExtendedDialog diag = new ExtendedDialog(Main.parent,
169                tr("Add Rectified Image"),
170                tr("Add Rectified Image"), tr("Cancel"))
171            .setContent(panel)
172            .setButtonIcons("OLmarker", "cancel");
173
174        // This repeatedly shows the dialog in case there has been an error.
175        // The loop is break;-ed if the users cancels
176        outer: while (true) {
177            diag.showDialog();
178            int answer = diag.getValue();
179            // Break loop when the user cancels
180            if (answer != 1) {
181                break;
182            }
183
184            String text = tfWmsUrl.getText().trim();
185            // Loop all services until we find the selected one
186            for (RectifierService s : services) {
187                if (!s.isSelected()) {
188                    continue;
189                }
190
191                // We've reached the custom WMS URL service
192                // Just set the URL and hope everything works out
193                if (s.wmsUrl.isEmpty()) {
194                    try {
195                        addWMSLayer(s.name + " (" + text + ')', text);
196                        break outer;
197                    } catch (IllegalStateException ex) {
198                        Logging.log(Logging.LEVEL_ERROR, ex);
199                    }
200                }
201
202                // First try to match if the entered string as an URL
203                Matcher m = s.urlRegEx.matcher(text);
204                if (m.find()) {
205                    String id = m.group(1);
206                    String newURL = s.wmsUrl.replaceAll("__s__", id);
207                    String title = s.name + " (" + id + ')';
208                    addWMSLayer(title, newURL);
209                    break outer;
210                }
211                // If not, look if it's a valid ID for the selected service
212                if (s.idValidator.matcher(text).matches()) {
213                    String newURL = s.wmsUrl.replaceAll("__s__", text);
214                    String title = s.name + " (" + text + ')';
215                    addWMSLayer(title, newURL);
216                    break outer;
217                }
218
219                // We've found the selected service, but the entered string isn't suitable for
220                // it. So quit checking the other radio buttons
221                break;
222            }
223
224            // and display an error message. The while loop ensures that the dialog pops up again
225            JOptionPane.showMessageDialog(Main.parent,
226                    tr("Couldn''t match the entered link or id to the selected service. Please try again."),
227                    tr("No valid WMS URL or id"),
228                    JOptionPane.ERROR_MESSAGE);
229            diag.setVisible(true);
230        }
231    }
232
233    /**
234     * Adds a WMS Layer with given title and URL
235     * @param title Name of the layer as it will shop up in the layer manager
236     * @param url URL to the WMS server
237     * @throws IllegalStateException if imagery time is neither HTML nor WMS
238     */
239    private static void addWMSLayer(String title, String url) {
240        ImageryInfo info = new ImageryInfo(title, url);
241        if (info.getImageryType() == ImageryType.WMS_ENDPOINT) {
242            try {
243                info = AddImageryLayerAction.getWMSLayerInfo(info);
244            } catch (IOException | WMSGetCapabilitiesException e) {
245                Logging.error(e);
246                JOptionPane.showMessageDialog(Main.parent, e.getMessage(), tr("No valid WMS URL or id"), JOptionPane.ERROR_MESSAGE);
247                return;
248            }
249        }
250        MainApplication.getLayerManager().addLayer(ImageryLayer.create(info));
251    }
252
253    @Override
254    protected void updateEnabledState() {
255        setEnabled(!getLayerManager().getLayers().isEmpty());
256    }
257}