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.data.imagery.ImageryInfo;
024import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
025import org.openstreetmap.josm.gui.ExtendedDialog;
026import org.openstreetmap.josm.gui.MainApplication;
027import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
028import org.openstreetmap.josm.gui.layer.ImageryLayer;
029import org.openstreetmap.josm.gui.widgets.JosmTextField;
030import org.openstreetmap.josm.gui.widgets.UrlLabel;
031import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException;
032import org.openstreetmap.josm.tools.GBC;
033import org.openstreetmap.josm.tools.Logging;
034import org.openstreetmap.josm.tools.Shortcut;
035
036/**
037 * Download rectified images from various services.
038 * @since 3715
039 */
040public class MapRectifierWMSmenuAction extends JosmAction {
041
042    /**
043     * Class that bundles all required information of a rectifier service
044     */
045    public static class RectifierService {
046        private final String name;
047        private final String url;
048        private final String wmsUrl;
049        private final Pattern urlRegEx;
050        private final Pattern idValidator;
051        private JRadioButton btn;
052
053        /**
054         * Constructs a new {@code RectifierService}.
055         * @param name Name of the rectifing service
056         * @param url URL to the service where users can register, upload, etc.
057         * @param wmsUrl URL to the WMS server where JOSM will grab the images. Insert __s__ where the ID should be placed
058         * @param urlRegEx a regular expression that determines if a given URL is one of the service and returns the WMS id if so
059         * @param idValidator regular expression that checks if a given ID is syntactically valid
060         */
061        public RectifierService(String name, String url, String wmsUrl, String urlRegEx, String idValidator) {
062            this.name = name;
063            this.url = url;
064            this.wmsUrl = wmsUrl;
065            this.urlRegEx = Pattern.compile(urlRegEx);
066            this.idValidator = Pattern.compile(idValidator);
067        }
068
069        private boolean isSelected() {
070            return btn.isSelected();
071        }
072    }
073
074    /**
075     * List of available rectifier services.
076     */
077    private final transient List<RectifierService> services = new ArrayList<>();
078
079    /**
080     * Constructs a new {@code MapRectifierWMSmenuAction}.
081     */
082    public MapRectifierWMSmenuAction() {
083        super(tr("Rectified Image..."),
084                "OLmarker",
085                tr("Download Rectified Images From Various Services"),
086                Shortcut.registerShortcut("imagery:rectimg",
087                        tr("Imagery: {0}", tr("Rectified Image...")),
088                        KeyEvent.CHAR_UNDEFINED, Shortcut.NONE),
089                true
090        );
091        setHelpId(ht("/Menu/Imagery"));
092
093        // Add default services
094        services.add(
095                new RectifierService("Metacarta Map Rectifier",
096                        "http://labs.metacarta.com/rectifier/",
097                        "http://labs.metacarta.com/rectifier/wms.cgi?id=__s__&srs=EPSG:4326"
098                        + "&Service=WMS&Version=1.1.0&Request=GetMap&format=image/png&",
099                        // This matches more than the "classic" WMS link, so users can pretty much
100                        // copy any link as long as it includes the ID
101                        "labs\\.metacarta\\.com/(?:.*?)(?:/|=)([0-9]+)(?:\\?|/|\\.|$)",
102                "^[0-9]+$")
103        );
104        services.add(
105                new RectifierService("Map Warper",
106                        "http://mapwarper.net/",
107                        "http://mapwarper.net/maps/wms/__s__?request=GetMap&version=1.1.1"
108                        + "&styles=&format=image/png&srs=epsg:4326&exceptions=application/vnd.ogc.se_inimage&",
109                        // This matches more than the "classic" WMS link, so users can pretty much
110                        // copy any link as long as it includes the ID
111                        "(?:mapwarper\\.net|warper\\.geothings\\.net)/(?:.*?)/([0-9]+)(?:\\?|/|\\.|$)",
112                "^[0-9]+$")
113        );
114
115        // This service serves the purpose of "just this once" without forcing the user
116        // to commit the link to the preferences
117
118        // Clipboard content gets trimmed, so matching whitespace only ensures that this
119        // service will never be selected automatically.
120        services.add(new RectifierService(tr("Custom WMS Link"), "", "", "^\\s+$", ""));
121    }
122
123    @Override
124    public void actionPerformed(ActionEvent e) {
125        if (!isEnabled()) return;
126        JPanel panel = new JPanel(new GridBagLayout());
127        panel.add(new JLabel(tr("Supported Rectifier Services:")), GBC.eol());
128
129        JosmTextField tfWmsUrl = new JosmTextField(30);
130
131        String clip = ClipboardUtils.getClipboardStringContent();
132        clip = clip == null ? "" : clip.trim();
133        ButtonGroup group = new ButtonGroup();
134
135        JRadioButton firstBtn = null;
136        for (RectifierService s : services) {
137            JRadioButton serviceBtn = new JRadioButton(s.name);
138            if (firstBtn == null) {
139                firstBtn = serviceBtn;
140            }
141            // Checks clipboard contents against current service if no match has been found yet.
142            // If the contents match, they will be inserted into the text field and the corresponding
143            // service will be pre-selected.
144            if (!clip.isEmpty() && tfWmsUrl.getText().isEmpty()
145                    && (s.urlRegEx.matcher(clip).find() || s.idValidator.matcher(clip).matches())) {
146                serviceBtn.setSelected(true);
147                tfWmsUrl.setText(clip);
148            }
149            s.btn = serviceBtn;
150            group.add(serviceBtn);
151            if (!s.url.isEmpty()) {
152                panel.add(serviceBtn, GBC.std());
153                panel.add(new UrlLabel(s.url, tr("Visit Homepage")), GBC.eol().anchor(GridBagConstraints.LINE_END));
154            } else {
155                panel.add(serviceBtn, GBC.eol().anchor(GridBagConstraints.LINE_START));
156            }
157        }
158
159        // Fallback in case no match was found
160        if (tfWmsUrl.getText().isEmpty() && firstBtn != null) {
161            firstBtn.setSelected(true);
162        }
163
164        panel.add(new JLabel(tr("WMS URL or Image ID:")), GBC.eol());
165        panel.add(tfWmsUrl, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
166
167        ExtendedDialog diag = new ExtendedDialog(MainApplication.getMainFrame(),
168                tr("Add Rectified Image"),
169                tr("Add Rectified Image"), tr("Cancel"))
170            .setContent(panel)
171            .configureContextsensitiveHelp(ht("/Menu/Imagery"), true)
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.replace("__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.replace("__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(MainApplication.getMainFrame(),
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 show 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                handleException(e);
246                return;
247            }
248        }
249        try {
250            MainApplication.getLayerManager().addLayer(ImageryLayer.create(info));
251        } catch (IllegalArgumentException e) {
252            handleException(e);
253        }
254    }
255
256    private static void handleException(Exception e) {
257        Logging.error(e);
258        JOptionPane.showMessageDialog(MainApplication.getMainFrame(),
259                e.getMessage(), tr("No valid WMS URL or id"), JOptionPane.ERROR_MESSAGE);
260    }
261
262    @Override
263    protected boolean listenToSelectionChange() {
264        return false;
265    }
266
267    @Override
268    protected void updateEnabledState() {
269        setEnabled(!getLayerManager().getLayers().isEmpty());
270    }
271}