001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io.imagery; 003 004import java.awt.HeadlessException; 005import java.io.IOException; 006import java.io.StringReader; 007import java.net.MalformedURLException; 008import java.net.URL; 009import java.util.ArrayList; 010import java.util.Collection; 011import java.util.Collections; 012import java.util.HashSet; 013import java.util.List; 014import java.util.Locale; 015import java.util.Set; 016import java.util.regex.Pattern; 017 018import javax.imageio.ImageIO; 019import javax.xml.parsers.DocumentBuilder; 020import javax.xml.parsers.ParserConfigurationException; 021 022import org.openstreetmap.josm.Main; 023import org.openstreetmap.josm.data.Bounds; 024import org.openstreetmap.josm.data.imagery.ImageryInfo; 025import org.openstreetmap.josm.data.projection.Projections; 026import org.openstreetmap.josm.tools.HttpClient; 027import org.openstreetmap.josm.tools.Predicate; 028import org.openstreetmap.josm.tools.Utils; 029import org.w3c.dom.Document; 030import org.w3c.dom.Element; 031import org.w3c.dom.Node; 032import org.w3c.dom.NodeList; 033import org.xml.sax.EntityResolver; 034import org.xml.sax.InputSource; 035import org.xml.sax.SAXException; 036 037public class WMSImagery { 038 039 public static class WMSGetCapabilitiesException extends Exception { 040 private final String incomingData; 041 042 /** 043 * Constructs a new {@code WMSGetCapabilitiesException} 044 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method) 045 * @param incomingData the answer from WMS server 046 */ 047 public WMSGetCapabilitiesException(Throwable cause, String incomingData) { 048 super(cause); 049 this.incomingData = incomingData; 050 } 051 052 /** 053 * Constructs a new {@code WMSGetCapabilitiesException} 054 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method 055 * @param incomingData the answer from the server 056 * @since 10520 057 */ 058 public WMSGetCapabilitiesException(String message, String incomingData) { 059 super(message); 060 this.incomingData = incomingData; 061 } 062 063 /** 064 * Returns the answer from WMS server. 065 * @return the answer from WMS server 066 */ 067 public String getIncomingData() { 068 return incomingData; 069 } 070 } 071 072 private List<LayerDetails> layers; 073 private URL serviceUrl; 074 private List<String> formats; 075 076 /** 077 * Returns the list of layers. 078 * @return the list of layers 079 */ 080 public List<LayerDetails> getLayers() { 081 return layers; 082 } 083 084 /** 085 * Returns the service URL. 086 * @return the service URL 087 */ 088 public URL getServiceUrl() { 089 return serviceUrl; 090 } 091 092 /** 093 * Returns the list of supported formats. 094 * @return the list of supported formats 095 */ 096 public List<String> getFormats() { 097 return Collections.unmodifiableList(formats); 098 } 099 100 public String getPreferredFormats() { 101 return formats.contains("image/jpeg") ? "image/jpeg" 102 : formats.contains("image/png") ? "image/png" 103 : formats.isEmpty() ? null 104 : formats.get(0); 105 } 106 107 String buildRootUrl() { 108 if (serviceUrl == null) { 109 return null; 110 } 111 StringBuilder a = new StringBuilder(serviceUrl.getProtocol()); 112 a.append("://").append(serviceUrl.getHost()); 113 if (serviceUrl.getPort() != -1) { 114 a.append(':').append(serviceUrl.getPort()); 115 } 116 a.append(serviceUrl.getPath()).append('?'); 117 if (serviceUrl.getQuery() != null) { 118 a.append(serviceUrl.getQuery()); 119 if (!serviceUrl.getQuery().isEmpty() && !serviceUrl.getQuery().endsWith("&")) { 120 a.append('&'); 121 } 122 } 123 return a.toString(); 124 } 125 126 public String buildGetMapUrl(Collection<LayerDetails> selectedLayers) { 127 return buildGetMapUrl(selectedLayers, "image/jpeg"); 128 } 129 130 public String buildGetMapUrl(Collection<LayerDetails> selectedLayers, String format) { 131 return buildRootUrl() 132 + "FORMAT=" + format + (imageFormatHasTransparency(format) ? "&TRANSPARENT=TRUE" : "") 133 + "&VERSION=1.1.1&SERVICE=WMS&REQUEST=GetMap&LAYERS=" 134 + Utils.join(",", Utils.transform(selectedLayers, new Utils.Function<LayerDetails, String>() { 135 @Override 136 public String apply(LayerDetails x) { 137 return x.ident; 138 } 139 })) 140 + "&STYLES=&SRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}"; 141 } 142 143 public void attemptGetCapabilities(String serviceUrlStr) throws MalformedURLException, IOException, WMSGetCapabilitiesException { 144 URL getCapabilitiesUrl = null; 145 try { 146 if (!Pattern.compile(".*GetCapabilities.*", Pattern.CASE_INSENSITIVE).matcher(serviceUrlStr).matches()) { 147 // If the url doesn't already have GetCapabilities, add it in 148 getCapabilitiesUrl = new URL(serviceUrlStr); 149 final String getCapabilitiesQuery = "VERSION=1.1.1&SERVICE=WMS&REQUEST=GetCapabilities"; 150 if (getCapabilitiesUrl.getQuery() == null) { 151 getCapabilitiesUrl = new URL(serviceUrlStr + '?' + getCapabilitiesQuery); 152 } else if (!getCapabilitiesUrl.getQuery().isEmpty() && !getCapabilitiesUrl.getQuery().endsWith("&")) { 153 getCapabilitiesUrl = new URL(serviceUrlStr + '&' + getCapabilitiesQuery); 154 } else { 155 getCapabilitiesUrl = new URL(serviceUrlStr + getCapabilitiesQuery); 156 } 157 } else { 158 // Otherwise assume it's a good URL and let the subsequent error 159 // handling systems deal with problems 160 getCapabilitiesUrl = new URL(serviceUrlStr); 161 } 162 serviceUrl = new URL(serviceUrlStr); 163 } catch (HeadlessException e) { 164 return; 165 } 166 167 Main.info("GET " + getCapabilitiesUrl); 168 final String incomingData = HttpClient.create(getCapabilitiesUrl).connect().fetchContent(); 169 Main.debug("Server response to Capabilities request:"); 170 Main.debug(incomingData); 171 172 try { 173 DocumentBuilder builder = Utils.newSafeDOMBuilder(); 174 builder.setEntityResolver(new EntityResolver() { 175 @Override 176 public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { 177 Main.info("Ignoring DTD " + publicId + ", " + systemId); 178 return new InputSource(new StringReader("")); 179 } 180 }); 181 Document document = builder.parse(new InputSource(new StringReader(incomingData))); 182 Element root = document.getDocumentElement(); 183 184 // Check if the request resulted in ServiceException 185 if ("ServiceException".equals(root.getTagName())) { 186 throw new WMSGetCapabilitiesException(root.getTextContent(), incomingData); 187 } 188 189 // Some WMS service URLs specify a different base URL for their GetMap service 190 Element child = getChild(root, "Capability"); 191 child = getChild(child, "Request"); 192 child = getChild(child, "GetMap"); 193 194 formats = new ArrayList<>(Utils.filter(Utils.transform(getChildren(child, "Format"), 195 new Utils.Function<Element, String>() { 196 @Override 197 public String apply(Element x) { 198 return x.getTextContent(); 199 } 200 }), 201 new Predicate<String>() { 202 @Override 203 public boolean evaluate(String format) { 204 boolean isFormatSupported = isImageFormatSupported(format); 205 if (!isFormatSupported) { 206 Main.info("Skipping unsupported image format {0}", format); 207 } 208 return isFormatSupported; 209 } 210 } 211 )); 212 213 child = getChild(child, "DCPType"); 214 child = getChild(child, "HTTP"); 215 child = getChild(child, "Get"); 216 child = getChild(child, "OnlineResource"); 217 if (child != null) { 218 String baseURL = child.getAttribute("xlink:href"); 219 if (baseURL != null && !baseURL.equals(serviceUrlStr)) { 220 Main.info("GetCapabilities specifies a different service URL: " + baseURL); 221 serviceUrl = new URL(baseURL); 222 } 223 } 224 225 Element capabilityElem = getChild(root, "Capability"); 226 List<Element> children = getChildren(capabilityElem, "Layer"); 227 layers = parseLayers(children, new HashSet<String>()); 228 } catch (MalformedURLException | ParserConfigurationException | SAXException e) { 229 throw new WMSGetCapabilitiesException(e, incomingData); 230 } 231 } 232 233 static boolean isImageFormatSupported(final String format) { 234 return ImageIO.getImageReadersByMIMEType(format).hasNext() 235 // handles image/tiff image/tiff8 image/geotiff image/geotiff8 236 || (format.startsWith("image/tiff") || format.startsWith("image/geotiff")) && ImageIO.getImageReadersBySuffix("tiff").hasNext() 237 || format.startsWith("image/png") && ImageIO.getImageReadersBySuffix("png").hasNext() 238 || format.startsWith("image/svg") && ImageIO.getImageReadersBySuffix("svg").hasNext() 239 || format.startsWith("image/bmp") && ImageIO.getImageReadersBySuffix("bmp").hasNext(); 240 } 241 242 static boolean imageFormatHasTransparency(final String format) { 243 return format != null && (format.startsWith("image/png") || format.startsWith("image/gif") 244 || format.startsWith("image/svg") || format.startsWith("image/tiff")); 245 } 246 247 public ImageryInfo toImageryInfo(String name, Collection<LayerDetails> selectedLayers) { 248 ImageryInfo i = new ImageryInfo(name, buildGetMapUrl(selectedLayers)); 249 if (selectedLayers != null) { 250 Set<String> proj = new HashSet<>(); 251 for (WMSImagery.LayerDetails l : selectedLayers) { 252 proj.addAll(l.getProjections()); 253 } 254 i.setServerProjections(proj); 255 } 256 return i; 257 } 258 259 private List<LayerDetails> parseLayers(List<Element> children, Set<String> parentCrs) { 260 List<LayerDetails> details = new ArrayList<>(children.size()); 261 for (Element element : children) { 262 details.add(parseLayer(element, parentCrs)); 263 } 264 return details; 265 } 266 267 private LayerDetails parseLayer(Element element, Set<String> parentCrs) { 268 String name = getChildContent(element, "Title", null, null); 269 String ident = getChildContent(element, "Name", null, null); 270 271 // The set of supported CRS/SRS for this layer 272 Set<String> crsList = new HashSet<>(); 273 // ...including this layer's already-parsed parent projections 274 crsList.addAll(parentCrs); 275 276 // Parse the CRS/SRS pulled out of this layer's XML element 277 // I think CRS and SRS are the same at this point 278 List<Element> crsChildren = getChildren(element, "CRS"); 279 crsChildren.addAll(getChildren(element, "SRS")); 280 for (Element child : crsChildren) { 281 String crs = (String) getContent(child); 282 if (!crs.isEmpty()) { 283 String upperCase = crs.trim().toUpperCase(Locale.ENGLISH); 284 crsList.add(upperCase); 285 } 286 } 287 288 // Check to see if any of the specified projections are supported by JOSM 289 boolean josmSupportsThisLayer = false; 290 for (String crs : crsList) { 291 josmSupportsThisLayer |= isProjSupported(crs); 292 } 293 294 Bounds bounds = null; 295 Element bboxElem = getChild(element, "EX_GeographicBoundingBox"); 296 if (bboxElem != null) { 297 // Attempt to use EX_GeographicBoundingBox for bounding box 298 double left = Double.parseDouble(getChildContent(bboxElem, "westBoundLongitude", null, null)); 299 double top = Double.parseDouble(getChildContent(bboxElem, "northBoundLatitude", null, null)); 300 double right = Double.parseDouble(getChildContent(bboxElem, "eastBoundLongitude", null, null)); 301 double bot = Double.parseDouble(getChildContent(bboxElem, "southBoundLatitude", null, null)); 302 bounds = new Bounds(bot, left, top, right); 303 } else { 304 // If that's not available, try LatLonBoundingBox 305 bboxElem = getChild(element, "LatLonBoundingBox"); 306 if (bboxElem != null) { 307 double left = Double.parseDouble(bboxElem.getAttribute("minx")); 308 double top = Double.parseDouble(bboxElem.getAttribute("maxy")); 309 double right = Double.parseDouble(bboxElem.getAttribute("maxx")); 310 double bot = Double.parseDouble(bboxElem.getAttribute("miny")); 311 bounds = new Bounds(bot, left, top, right); 312 } 313 } 314 315 List<Element> layerChildren = getChildren(element, "Layer"); 316 List<LayerDetails> childLayers = parseLayers(layerChildren, crsList); 317 318 return new LayerDetails(name, ident, crsList, josmSupportsThisLayer, bounds, childLayers); 319 } 320 321 private static boolean isProjSupported(String crs) { 322 return Projections.getProjectionByCode(crs) != null; 323 } 324 325 private static String getChildContent(Element parent, String name, String missing, String empty) { 326 Element child = getChild(parent, name); 327 if (child == null) 328 return missing; 329 else { 330 String content = (String) getContent(child); 331 return (!content.isEmpty()) ? content : empty; 332 } 333 } 334 335 private static Object getContent(Element element) { 336 NodeList nl = element.getChildNodes(); 337 StringBuilder content = new StringBuilder(); 338 for (int i = 0; i < nl.getLength(); i++) { 339 Node node = nl.item(i); 340 switch (node.getNodeType()) { 341 case Node.ELEMENT_NODE: 342 return node; 343 case Node.CDATA_SECTION_NODE: 344 case Node.TEXT_NODE: 345 content.append(node.getNodeValue()); 346 break; 347 default: // Do nothing 348 } 349 } 350 return content.toString().trim(); 351 } 352 353 private static List<Element> getChildren(Element parent, String name) { 354 List<Element> retVal = new ArrayList<>(); 355 if (parent != null) { 356 for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) { 357 if (child instanceof Element && name.equals(child.getNodeName())) { 358 retVal.add((Element) child); 359 } 360 } 361 } 362 return retVal; 363 } 364 365 private static Element getChild(Element parent, String name) { 366 if (parent == null) 367 return null; 368 for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) { 369 if (child instanceof Element && name.equals(child.getNodeName())) 370 return (Element) child; 371 } 372 return null; 373 } 374 375 public static class LayerDetails { 376 377 public final String name; 378 public final String ident; 379 public final List<LayerDetails> children; 380 public final Bounds bounds; 381 public final Set<String> crsList; 382 public final boolean supported; 383 384 public LayerDetails(String name, String ident, Set<String> crsList, 385 boolean supportedLayer, Bounds bounds, 386 List<LayerDetails> childLayers) { 387 this.name = name; 388 this.ident = ident; 389 this.supported = supportedLayer; 390 this.children = childLayers; 391 this.bounds = bounds; 392 this.crsList = crsList; 393 } 394 395 public boolean isSupported() { 396 return this.supported; 397 } 398 399 public Set<String> getProjections() { 400 return crsList; 401 } 402 403 @Override 404 public String toString() { 405 if (this.name == null || this.name.isEmpty()) 406 return this.ident; 407 else 408 return this.name; 409 } 410 } 411}