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