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 }