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