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