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