001// License: GPL. For details, see Readme.txt file. 002package org.openstreetmap.gui.jmapviewer.tilesources; 003 004import java.awt.Image; 005import java.io.IOException; 006import java.io.InputStream; 007import java.net.MalformedURLException; 008import java.net.URL; 009import java.util.ArrayList; 010import java.util.List; 011import java.util.Locale; 012import java.util.concurrent.Callable; 013import java.util.concurrent.ExecutionException; 014import java.util.concurrent.Future; 015import java.util.concurrent.FutureTask; 016import java.util.concurrent.TimeUnit; 017import java.util.concurrent.TimeoutException; 018import java.util.regex.Pattern; 019 020import javax.imageio.ImageIO; 021import javax.xml.parsers.DocumentBuilder; 022import javax.xml.parsers.DocumentBuilderFactory; 023import javax.xml.parsers.ParserConfigurationException; 024import javax.xml.xpath.XPath; 025import javax.xml.xpath.XPathConstants; 026import javax.xml.xpath.XPathExpression; 027import javax.xml.xpath.XPathExpressionException; 028import javax.xml.xpath.XPathFactory; 029 030import org.openstreetmap.gui.jmapviewer.Coordinate; 031import org.openstreetmap.gui.jmapviewer.JMapViewer; 032import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 033import org.w3c.dom.Document; 034import org.w3c.dom.Node; 035import org.w3c.dom.NodeList; 036import org.xml.sax.InputSource; 037import org.xml.sax.SAXException; 038 039public class BingAerialTileSource extends AbstractTMSTileSource { 040 041 private static final String API_KEY = "Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU"; 042 private static volatile Future<List<Attribution>> attributions; // volatile is required for getAttribution(), see below. 043 private static String imageUrlTemplate; 044 private static Integer imageryZoomMax; 045 private static String[] subdomains; 046 047 private static final Pattern subdomainPattern = Pattern.compile("\\{subdomain\\}"); 048 private static final Pattern quadkeyPattern = Pattern.compile("\\{quadkey\\}"); 049 private static final Pattern culturePattern = Pattern.compile("\\{culture\\}"); 050 private String brandLogoUri; 051 052 /** 053 * Constructs a new {@code BingAerialTileSource}. 054 */ 055 public BingAerialTileSource() { 056 super(new TileSourceInfo("Bing", null, null)); 057 } 058 059 /** 060 * Constructs a new {@code BingAerialTileSource}. 061 * @param info imagery info 062 */ 063 public BingAerialTileSource(TileSourceInfo info) { 064 super(info); 065 } 066 067 protected static class Attribution { 068 private String attribution; 069 private int minZoom; 070 private int maxZoom; 071 private Coordinate min; 072 private Coordinate max; 073 } 074 075 @Override 076 public String getTileUrl(int zoom, int tilex, int tiley) throws IOException { 077 // make sure that attribution is loaded. otherwise subdomains is null. 078 if (getAttribution() == null) 079 throw new IOException("Attribution is not loaded yet"); 080 081 int t = (zoom + tilex + tiley) % subdomains.length; 082 String subdomain = subdomains[t]; 083 084 String url = imageUrlTemplate; 085 url = subdomainPattern.matcher(url).replaceAll(subdomain); 086 url = quadkeyPattern.matcher(url).replaceAll(computeQuadTree(zoom, tilex, tiley)); 087 088 return url; 089 } 090 091 protected URL getAttributionUrl() throws MalformedURLException { 092 return new URL("http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?include=ImageryProviders&output=xml&key=" 093 + API_KEY); 094 } 095 096 protected List<Attribution> parseAttributionText(InputSource xml) throws IOException { 097 try { 098 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 099 DocumentBuilder builder = factory.newDocumentBuilder(); 100 Document document = builder.parse(xml); 101 102 XPathFactory xPathFactory = XPathFactory.newInstance(); 103 XPath xpath = xPathFactory.newXPath(); 104 imageUrlTemplate = xpath.compile("//ImageryMetadata/ImageUrl/text()").evaluate(document); 105 imageUrlTemplate = culturePattern.matcher(imageUrlTemplate).replaceAll(Locale.getDefault().toString()); 106 imageryZoomMax = Integer.valueOf(xpath.compile("//ImageryMetadata/ZoomMax/text()").evaluate(document)); 107 108 NodeList subdomainTxt = (NodeList) xpath.compile("//ImageryMetadata/ImageUrlSubdomains/string/text()") 109 .evaluate(document, XPathConstants.NODESET); 110 subdomains = new String[subdomainTxt.getLength()]; 111 for (int i = 0; i < subdomainTxt.getLength(); i++) { 112 subdomains[i] = subdomainTxt.item(i).getNodeValue(); 113 } 114 115 brandLogoUri = xpath.compile("/Response/BrandLogoUri/text()").evaluate(document); 116 117 XPathExpression attributionXpath = xpath.compile("Attribution/text()"); 118 XPathExpression coverageAreaXpath = xpath.compile("CoverageArea"); 119 XPathExpression zoomMinXpath = xpath.compile("ZoomMin/text()"); 120 XPathExpression zoomMaxXpath = xpath.compile("ZoomMax/text()"); 121 XPathExpression southLatXpath = xpath.compile("BoundingBox/SouthLatitude/text()"); 122 XPathExpression westLonXpath = xpath.compile("BoundingBox/WestLongitude/text()"); 123 XPathExpression northLatXpath = xpath.compile("BoundingBox/NorthLatitude/text()"); 124 XPathExpression eastLonXpath = xpath.compile("BoundingBox/EastLongitude/text()"); 125 126 NodeList imageryProviderNodes = (NodeList) xpath.compile("//ImageryMetadata/ImageryProvider") 127 .evaluate(document, XPathConstants.NODESET); 128 List<Attribution> attributions = new ArrayList<>(imageryProviderNodes.getLength()); 129 for (int i = 0; i < imageryProviderNodes.getLength(); i++) { 130 Node providerNode = imageryProviderNodes.item(i); 131 132 String attribution = attributionXpath.evaluate(providerNode); 133 134 NodeList coverageAreaNodes = (NodeList) coverageAreaXpath.evaluate(providerNode, XPathConstants.NODESET); 135 for (int j = 0; j < coverageAreaNodes.getLength(); j++) { 136 Node areaNode = coverageAreaNodes.item(j); 137 Attribution attr = new Attribution(); 138 attr.attribution = attribution; 139 140 attr.maxZoom = Integer.parseInt(zoomMaxXpath.evaluate(areaNode)); 141 attr.minZoom = Integer.parseInt(zoomMinXpath.evaluate(areaNode)); 142 143 Double southLat = Double.valueOf(southLatXpath.evaluate(areaNode)); 144 Double northLat = Double.valueOf(northLatXpath.evaluate(areaNode)); 145 Double westLon = Double.valueOf(westLonXpath.evaluate(areaNode)); 146 Double eastLon = Double.valueOf(eastLonXpath.evaluate(areaNode)); 147 attr.min = new Coordinate(southLat, westLon); 148 attr.max = new Coordinate(northLat, eastLon); 149 150 attributions.add(attr); 151 } 152 } 153 154 return attributions; 155 } catch (SAXException e) { 156 System.err.println("Could not parse Bing aerials attribution metadata."); 157 e.printStackTrace(); 158 } catch (ParserConfigurationException e) { 159 e.printStackTrace(); 160 } catch (XPathExpressionException e) { 161 e.printStackTrace(); 162 } 163 return null; 164 } 165 166 @Override 167 public int getMaxZoom() { 168 if (imageryZoomMax != null) 169 return imageryZoomMax; 170 else 171 return 22; 172 } 173 174 @Override 175 public boolean requiresAttribution() { 176 return true; 177 } 178 179 @Override 180 public String getAttributionLinkURL() { 181 //return "http://bing.com/maps" 182 // FIXME: I've set attributionLinkURL temporarily to ToU URL to comply with bing ToU 183 // (the requirement is that we have such a link at the bottom of the window) 184 return "http://go.microsoft.com/?linkid=9710837"; 185 } 186 187 @Override 188 public Image getAttributionImage() { 189 try { 190 final InputStream imageResource = JMapViewer.class.getResourceAsStream("images/bing_maps.png"); 191 if (imageResource != null) { 192 return ImageIO.read(imageResource); 193 } else { 194 // Some Linux distributions (like Debian) will remove Bing logo from sources, so get it at runtime 195 for (int i = 0; i < 5 && getAttribution() == null; i++) { 196 // Makes sure attribution is loaded 197 if (JMapViewer.debug) { 198 System.out.println("Bing attribution attempt " + (i+1)); 199 } 200 } 201 if (brandLogoUri != null && !brandLogoUri.isEmpty()) { 202 System.out.println("Reading Bing logo from "+brandLogoUri); 203 return ImageIO.read(new URL(brandLogoUri)); 204 } 205 } 206 } catch (IOException e) { 207 System.err.println("Error while retrieving Bing logo: "+e.getMessage()); 208 } 209 return null; 210 } 211 212 @Override 213 public String getAttributionImageURL() { 214 return "http://opengeodata.org/microsoft-imagery-details"; 215 } 216 217 @Override 218 public String getTermsOfUseText() { 219 return null; 220 } 221 222 @Override 223 public String getTermsOfUseURL() { 224 return "http://opengeodata.org/microsoft-imagery-details"; 225 } 226 227 protected Callable<List<Attribution>> getAttributionLoaderCallable() { 228 return new Callable<List<Attribution>>() { 229 230 @Override 231 public List<Attribution> call() throws Exception { 232 int waitTimeSec = 1; 233 while (true) { 234 try { 235 InputSource xml = new InputSource(getAttributionUrl().openStream()); 236 List<Attribution> r = parseAttributionText(xml); 237 System.out.println("Successfully loaded Bing attribution data."); 238 return r; 239 } catch (IOException ex) { 240 System.err.println("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds."); 241 Thread.sleep(waitTimeSec * 1000L); 242 waitTimeSec *= 2; 243 } 244 } 245 } 246 }; 247 } 248 249 protected List<Attribution> getAttribution() { 250 if (attributions == null) { 251 // see http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 252 synchronized (BingAerialTileSource.class) { 253 if (attributions == null) { 254 final FutureTask<List<Attribution>> loader = new FutureTask<>(getAttributionLoaderCallable()); 255 new Thread(loader, "bing-attribution-loader").start(); 256 attributions = loader; 257 } 258 } 259 } 260 try { 261 return attributions.get(0, TimeUnit.MILLISECONDS); 262 } catch (TimeoutException ex) { 263 System.err.println("Bing: attribution data is not yet loaded."); 264 } catch (ExecutionException ex) { 265 throw new RuntimeException(ex.getCause()); 266 } catch (InterruptedException ign) { 267 System.err.println("InterruptedException: " + ign.getMessage()); 268 } 269 return null; 270 } 271 272 @Override 273 public String getAttributionText(int zoom, ICoordinate topLeft, ICoordinate botRight) { 274 try { 275 final List<Attribution> data = getAttribution(); 276 if (data == null) 277 return "Error loading Bing attribution data"; 278 StringBuilder a = new StringBuilder(); 279 for (Attribution attr : data) { 280 if (zoom <= attr.maxZoom && zoom >= attr.minZoom) { 281 if (topLeft.getLon() < attr.max.getLon() && botRight.getLon() > attr.min.getLon() 282 && topLeft.getLat() > attr.min.getLat() && botRight.getLat() < attr.max.getLat()) { 283 a.append(attr.attribution); 284 a.append(' '); 285 } 286 } 287 } 288 return a.toString(); 289 } catch (Exception e) { 290 e.printStackTrace(); 291 } 292 return "Error loading Bing attribution data"; 293 } 294 295 private static String computeQuadTree(int zoom, int tilex, int tiley) { 296 StringBuilder k = new StringBuilder(); 297 for (int i = zoom; i > 0; i--) { 298 char digit = 48; 299 int mask = 1 << (i - 1); 300 if ((tilex & mask) != 0) { 301 digit += 1; 302 } 303 if ((tiley & mask) != 0) { 304 digit += 2; 305 } 306 k.append(digit); 307 } 308 return k.toString(); 309 } 310}