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