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.EAST)); 154 } else { 155 panel.add(serviceBtn, GBC.eol().anchor(GridBagConstraints.WEST)); 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 void updateEnabledState() { 264 setEnabled(!getLayerManager().getLayers().isEmpty()); 265 } 266}