source: osm/applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/BingAerialTileSource.java@ 31122

Last change on this file since 31122 was 31122, checked in by bastik, 9 years ago

applied #josm10454 - Mapbox "empty" tile (imagery with zoom level > 17) (patch by wiktorn)

  • Property svn:eol-style set to native
File size: 12.0 KB
Line 
1// License: GPL. For details, see Readme.txt file.
2package org.openstreetmap.gui.jmapviewer.tilesources;
3
4import java.awt.Image;
5import java.io.IOException;
6import java.io.InputStream;
7import java.net.MalformedURLException;
8import java.net.URL;
9import java.util.ArrayList;
10import java.util.List;
11import java.util.Locale;
12import java.util.concurrent.Callable;
13import java.util.concurrent.ExecutionException;
14import java.util.concurrent.Executors;
15import java.util.concurrent.Future;
16import java.util.concurrent.TimeUnit;
17import java.util.concurrent.TimeoutException;
18import java.util.regex.Pattern;
19
20import javax.imageio.ImageIO;
21import javax.xml.parsers.DocumentBuilder;
22import javax.xml.parsers.DocumentBuilderFactory;
23import javax.xml.parsers.ParserConfigurationException;
24import javax.xml.xpath.XPath;
25import javax.xml.xpath.XPathConstants;
26import javax.xml.xpath.XPathExpression;
27import javax.xml.xpath.XPathExpressionException;
28import javax.xml.xpath.XPathFactory;
29
30import org.openstreetmap.gui.jmapviewer.Coordinate;
31import org.openstreetmap.gui.jmapviewer.JMapViewer;
32import org.w3c.dom.Document;
33import org.w3c.dom.Node;
34import org.w3c.dom.NodeList;
35import org.xml.sax.InputSource;
36import org.xml.sax.SAXException;
37
38public class BingAerialTileSource extends AbstractTMSTileSource {
39
40 private static String API_KEY = "Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU";
41 private static volatile Future<List<Attribution>> attributions; // volatile is required for getAttribution(), see below.
42 private static String imageUrlTemplate;
43 private static Integer imageryZoomMax;
44 private static String[] subdomains;
45
46 private static final Pattern subdomainPattern = Pattern.compile("\\{subdomain\\}");
47 private static final Pattern quadkeyPattern = Pattern.compile("\\{quadkey\\}");
48 private static final Pattern culturePattern = Pattern.compile("\\{culture\\}");
49 private String brandLogoUri = null;
50
51 /**
52 * Constructs a new {@code BingAerialTileSource}.
53 */
54 public BingAerialTileSource() {
55 super(new TileSourceInfo("Bing", null, null));
56 }
57
58 public BingAerialTileSource(TileSourceInfo info) {
59 super(info);
60 }
61
62 protected class Attribution {
63 String attribution;
64 int minZoom;
65 int maxZoom;
66 Coordinate min;
67 Coordinate max;
68 }
69
70 @Override
71 public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
72 // make sure that attribution is loaded. otherwise subdomains is null.
73 if (getAttribution() == null)
74 throw new IOException("Attribution is not loaded yet");
75
76 int t = (zoom + tilex + tiley) % subdomains.length;
77 String subdomain = subdomains[t];
78
79 String url = imageUrlTemplate;
80 url = subdomainPattern.matcher(url).replaceAll(subdomain);
81 url = quadkeyPattern.matcher(url).replaceAll(computeQuadTree(zoom, tilex, tiley));
82
83 return url;
84 }
85
86 protected URL getAttributionUrl() throws MalformedURLException {
87 return new URL("http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?include=ImageryProviders&output=xml&key="
88 + API_KEY);
89 }
90
91 protected List<Attribution> parseAttributionText(InputSource xml) throws IOException {
92 try {
93 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
94 DocumentBuilder builder = factory.newDocumentBuilder();
95 Document document = builder.parse(xml);
96
97 XPathFactory xPathFactory = XPathFactory.newInstance();
98 XPath xpath = xPathFactory.newXPath();
99 imageUrlTemplate = xpath.compile("//ImageryMetadata/ImageUrl/text()").evaluate(document);
100 imageUrlTemplate = culturePattern.matcher(imageUrlTemplate).replaceAll(Locale.getDefault().toString());
101 imageryZoomMax = Integer.parseInt(xpath.compile("//ImageryMetadata/ZoomMax/text()").evaluate(document));
102
103 NodeList subdomainTxt = (NodeList) xpath.compile("//ImageryMetadata/ImageUrlSubdomains/string/text()").evaluate(document, XPathConstants.NODESET);
104 subdomains = new String[subdomainTxt.getLength()];
105 for(int i = 0; i < subdomainTxt.getLength(); i++) {
106 subdomains[i] = subdomainTxt.item(i).getNodeValue();
107 }
108
109 brandLogoUri = xpath.compile("/Response/BrandLogoUri/text()").evaluate(document);
110
111 XPathExpression attributionXpath = xpath.compile("Attribution/text()");
112 XPathExpression coverageAreaXpath = xpath.compile("CoverageArea");
113 XPathExpression zoomMinXpath = xpath.compile("ZoomMin/text()");
114 XPathExpression zoomMaxXpath = xpath.compile("ZoomMax/text()");
115 XPathExpression southLatXpath = xpath.compile("BoundingBox/SouthLatitude/text()");
116 XPathExpression westLonXpath = xpath.compile("BoundingBox/WestLongitude/text()");
117 XPathExpression northLatXpath = xpath.compile("BoundingBox/NorthLatitude/text()");
118 XPathExpression eastLonXpath = xpath.compile("BoundingBox/EastLongitude/text()");
119
120 NodeList imageryProviderNodes = (NodeList) xpath.compile("//ImageryMetadata/ImageryProvider").evaluate(document, XPathConstants.NODESET);
121 List<Attribution> attributions = new ArrayList<>(imageryProviderNodes.getLength());
122 for (int i = 0; i < imageryProviderNodes.getLength(); i++) {
123 Node providerNode = imageryProviderNodes.item(i);
124
125 String attribution = attributionXpath.evaluate(providerNode);
126
127 NodeList coverageAreaNodes = (NodeList) coverageAreaXpath.evaluate(providerNode, XPathConstants.NODESET);
128 for(int j = 0; j < coverageAreaNodes.getLength(); j++) {
129 Node areaNode = coverageAreaNodes.item(j);
130 Attribution attr = new Attribution();
131 attr.attribution = attribution;
132
133 attr.maxZoom = Integer.parseInt(zoomMaxXpath.evaluate(areaNode));
134 attr.minZoom = Integer.parseInt(zoomMinXpath.evaluate(areaNode));
135
136 Double southLat = Double.parseDouble(southLatXpath.evaluate(areaNode));
137 Double northLat = Double.parseDouble(northLatXpath.evaluate(areaNode));
138 Double westLon = Double.parseDouble(westLonXpath.evaluate(areaNode));
139 Double eastLon = Double.parseDouble(eastLonXpath.evaluate(areaNode));
140 attr.min = new Coordinate(southLat, westLon);
141 attr.max = new Coordinate(northLat, eastLon);
142
143 attributions.add(attr);
144 }
145 }
146
147 return attributions;
148 } catch (SAXException e) {
149 System.err.println("Could not parse Bing aerials attribution metadata.");
150 e.printStackTrace();
151 } catch (ParserConfigurationException e) {
152 e.printStackTrace();
153 } catch (XPathExpressionException e) {
154 e.printStackTrace();
155 }
156 return null;
157 }
158
159 @Override
160 public int getMaxZoom() {
161 if(imageryZoomMax != null)
162 return imageryZoomMax;
163 else
164 return 22;
165 }
166
167 @Override
168 public TileUpdate getTileUpdate() {
169 return TileUpdate.IfNoneMatch;
170 }
171
172 @Override
173 public boolean requiresAttribution() {
174 return true;
175 }
176
177 @Override
178 public String getAttributionLinkURL() {
179 //return "http://bing.com/maps"
180 // FIXME: I've set attributionLinkURL temporarily to ToU URL to comply with bing ToU
181 // (the requirement is that we have such a link at the bottom of the window)
182 return "http://go.microsoft.com/?linkid=9710837";
183 }
184
185 @Override
186 public Image getAttributionImage() {
187 try {
188 final InputStream imageResource = JMapViewer.class.getResourceAsStream("images/bing_maps.png");
189 if (imageResource != null) {
190 return ImageIO.read(imageResource);
191 } else {
192 // Some Linux distributions (like Debian) will remove Bing logo from sources, so get it at runtime
193 for (int i = 0; i < 5 && getAttribution() == null; i++) {
194 // Makes sure attribution is loaded
195 }
196 if (brandLogoUri != null && !brandLogoUri.isEmpty()) {
197 System.out.println("Reading Bing logo from "+brandLogoUri);
198 return ImageIO.read(new URL(brandLogoUri));
199 }
200 }
201 } catch (IOException e) {
202 System.err.println("Error while retrieving Bing logo: "+e.getMessage());
203 }
204 return null;
205 }
206
207 @Override
208 public String getAttributionImageURL() {
209 return "http://opengeodata.org/microsoft-imagery-details";
210 }
211
212 @Override
213 public String getTermsOfUseText() {
214 return null;
215 }
216
217 @Override
218 public String getTermsOfUseURL() {
219 return "http://opengeodata.org/microsoft-imagery-details";
220 }
221
222 protected Callable<List<Attribution>> getAttributionLoaderCallable() {
223 return new Callable<List<Attribution>>() {
224
225 @Override
226 public List<Attribution> call() throws Exception {
227 int waitTimeSec = 1;
228 while (true) {
229 try {
230 InputSource xml = new InputSource(getAttributionUrl().openStream());
231 List<Attribution> r = parseAttributionText(xml);
232 System.out.println("Successfully loaded Bing attribution data.");
233 return r;
234 } catch (IOException ex) {
235 System.err.println("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds.");
236 Thread.sleep(waitTimeSec * 1000L);
237 waitTimeSec *= 2;
238 }
239 }
240 }
241 };
242 }
243
244 protected List<Attribution> getAttribution() {
245 if (attributions == null) {
246 // see http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
247 synchronized (BingAerialTileSource.class) {
248 if (attributions == null) {
249 attributions = Executors.newSingleThreadExecutor().submit(getAttributionLoaderCallable());
250 }
251 }
252 }
253 try {
254 return attributions.get(1000, TimeUnit.MILLISECONDS);
255 } catch (TimeoutException ex) {
256 System.err.println("Bing: attribution data is not yet loaded.");
257 } catch (ExecutionException ex) {
258 throw new RuntimeException(ex.getCause());
259 } catch (InterruptedException ign) {
260 }
261 return null;
262 }
263
264 @Override
265 public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) {
266 try {
267 final List<Attribution> data = getAttribution();
268 if (data == null)
269 return "Error loading Bing attribution data";
270 StringBuilder a = new StringBuilder();
271 for (Attribution attr : data) {
272 if (zoom <= attr.maxZoom && zoom >= attr.minZoom) {
273 if (topLeft.getLon() < attr.max.getLon() && botRight.getLon() > attr.min.getLon()
274 && topLeft.getLat() > attr.min.getLat() && botRight.getLat() < attr.max.getLat()) {
275 a.append(attr.attribution);
276 a.append(" ");
277 }
278 }
279 }
280 return a.toString();
281 } catch (Exception e) {
282 e.printStackTrace();
283 }
284 return "Error loading Bing attribution data";
285 }
286
287 static String computeQuadTree(int zoom, int tilex, int tiley) {
288 StringBuilder k = new StringBuilder();
289 for (int i = zoom; i > 0; i--) {
290 char digit = 48;
291 int mask = 1 << (i - 1);
292 if ((tilex & mask) != 0) {
293 digit += 1;
294 }
295 if ((tiley & mask) != 0) {
296 digit += 2;
297 }
298 k.append(digit);
299 }
300 return k.toString();
301 }
302}
Note: See TracBrowser for help on using the repository browser.