| 1 | package org.openstreetmap.gui.jmapviewer.tilesources;
|
|---|
| 2 |
|
|---|
| 3 | import java.awt.Image;
|
|---|
| 4 | import java.io.IOException;
|
|---|
| 5 | import java.io.InputStream;
|
|---|
| 6 | import java.net.URL;
|
|---|
| 7 | import java.net.URLConnection;
|
|---|
| 8 | import java.util.ArrayList;
|
|---|
| 9 | import java.util.List;
|
|---|
| 10 | import java.util.Locale;
|
|---|
| 11 | import java.util.concurrent.Callable;
|
|---|
| 12 | import java.util.concurrent.Executors;
|
|---|
| 13 | import java.util.concurrent.Future;
|
|---|
| 14 | import java.util.regex.Pattern;
|
|---|
| 15 |
|
|---|
| 16 | import javax.imageio.ImageIO;
|
|---|
| 17 | import javax.xml.parsers.DocumentBuilder;
|
|---|
| 18 | import javax.xml.parsers.DocumentBuilderFactory;
|
|---|
| 19 | import javax.xml.parsers.ParserConfigurationException;
|
|---|
| 20 |
|
|---|
| 21 | import org.openstreetmap.gui.jmapviewer.Coordinate;
|
|---|
| 22 | import org.w3c.dom.Document;
|
|---|
| 23 | import org.w3c.dom.Element;
|
|---|
| 24 | import org.w3c.dom.Node;
|
|---|
| 25 | import org.w3c.dom.NodeList;
|
|---|
| 26 | import org.xml.sax.SAXException;
|
|---|
| 27 |
|
|---|
| 28 | public class BingAerialTileSource extends AbstractTSMTileSource {
|
|---|
| 29 | private static String API_KEY = "Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU";
|
|---|
| 30 | private static Future<List<Attribution>> attributions;
|
|---|
| 31 | private static String imageUrlTemplate;
|
|---|
| 32 | private static Integer imageryZoomMax;
|
|---|
| 33 | private static String[] subdomains;
|
|---|
| 34 |
|
|---|
| 35 | private static final Pattern subdomainPattern = Pattern.compile("\\{subdomain\\}");
|
|---|
| 36 | private static final Pattern quadkeyPattern = Pattern.compile("\\{quadkey\\}");
|
|---|
| 37 | private static final Pattern culturePattern = Pattern.compile("\\{culture\\}");
|
|---|
| 38 |
|
|---|
| 39 | public BingAerialTileSource() {
|
|---|
| 40 | super("Bing Aerial Maps", "http://example.com/");
|
|---|
| 41 |
|
|---|
| 42 | if (attributions == null) {
|
|---|
| 43 | attributions = Executors.newSingleThreadExecutor().submit(new Callable<List<Attribution>>() {
|
|---|
| 44 | public List<Attribution> call() throws Exception {
|
|---|
| 45 | List<Attribution> attrs = new ArrayList<Attribution>();
|
|---|
| 46 | int waitTime = 1;
|
|---|
| 47 | do {
|
|---|
| 48 |
|
|---|
| 49 | try {
|
|---|
| 50 | attrs = loadAttributionText();
|
|---|
| 51 | System.out.println("Successfully loaded Bing attribution data.");
|
|---|
| 52 | return attrs;
|
|---|
| 53 | } catch (TileLoadException e) {
|
|---|
| 54 | System.err.println(e.getMessage());
|
|---|
| 55 | return attrs;
|
|---|
| 56 | } catch(IOException e) {
|
|---|
| 57 | System.err.println("Could not connect to Bing API. Will retry in " + waitTime + " seconds.");
|
|---|
| 58 | Thread.sleep(waitTime * 1000L);
|
|---|
| 59 | waitTime *= 2;
|
|---|
| 60 | }
|
|---|
| 61 |
|
|---|
| 62 | } while(true);
|
|---|
| 63 | }
|
|---|
| 64 | });
|
|---|
| 65 | }
|
|---|
| 66 | }
|
|---|
| 67 |
|
|---|
| 68 | class Attribution {
|
|---|
| 69 | String attribution;
|
|---|
| 70 | int minZoom;
|
|---|
| 71 | int maxZoom;
|
|---|
| 72 | Coordinate min;
|
|---|
| 73 | Coordinate max;
|
|---|
| 74 | }
|
|---|
| 75 |
|
|---|
| 76 | @Override
|
|---|
| 77 | public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
|
|---|
| 78 | int t = (zoom + tilex + tiley) % subdomains.length;
|
|---|
| 79 | String subdomain = subdomains[t];
|
|---|
| 80 |
|
|---|
| 81 | String url = new String(imageUrlTemplate);
|
|---|
| 82 | url = subdomainPattern.matcher(url).replaceAll(subdomain);
|
|---|
| 83 | url = quadkeyPattern.matcher(url).replaceAll(computeQuadTree(zoom, tilex, tiley));
|
|---|
| 84 |
|
|---|
| 85 | return url;
|
|---|
| 86 | }
|
|---|
| 87 |
|
|---|
| 88 | /*private List<Attribution> loadAttributionText() throws IOException {
|
|---|
| 89 | try {
|
|---|
| 90 | URL u = new URL("http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?include=ImageryProviders&output=xml&key="
|
|---|
| 91 | + API_KEY);
|
|---|
| 92 | URLConnection conn = u.openConnection();
|
|---|
| 93 |
|
|---|
| 94 | InputStream stream = conn.getInputStream();
|
|---|
| 95 |
|
|---|
| 96 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
|---|
| 97 | DocumentBuilder builder = factory.newDocumentBuilder();
|
|---|
| 98 | Document document = builder.parse(stream);
|
|---|
| 99 |
|
|---|
| 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 | XPathExpression attributionXpath = xpath.compile("Attribution/text()");
|
|---|
| 113 | XPathExpression coverageAreaXpath = xpath.compile("CoverageArea");
|
|---|
| 114 | XPathExpression zoomMinXpath = xpath.compile("ZoomMin/text()");
|
|---|
| 115 | XPathExpression zoomMaxXpath = xpath.compile("ZoomMax/text()");
|
|---|
| 116 | XPathExpression southLatXpath = xpath.compile("BoundingBox/SouthLatitude/text()");
|
|---|
| 117 | XPathExpression westLonXpath = xpath.compile("BoundingBox/WestLongitude/text()");
|
|---|
| 118 | XPathExpression northLatXpath = xpath.compile("BoundingBox/NorthLatitude/text()");
|
|---|
| 119 | XPathExpression eastLonXpath = xpath.compile("BoundingBox/EastLongitude/text()");
|
|---|
| 120 |
|
|---|
| 121 | NodeList imageryProviderNodes = (NodeList) xpath.compile("//ImageryMetadata/ImageryProvider").evaluate(document, XPathConstants.NODESET);
|
|---|
| 122 | List<Attribution> attributions = new ArrayList<Attribution>(imageryProviderNodes.getLength());
|
|---|
| 123 | for (int i = 0; i < imageryProviderNodes.getLength(); i++) {
|
|---|
| 124 | Node providerNode = imageryProviderNodes.item(i);
|
|---|
| 125 |
|
|---|
| 126 | String attribution = attributionXpath.evaluate(providerNode);
|
|---|
| 127 |
|
|---|
| 128 | NodeList coverageAreaNodes = (NodeList) coverageAreaXpath.evaluate(providerNode, XPathConstants.NODESET);
|
|---|
| 129 | for(int j = 0; j < coverageAreaNodes.getLength(); j++) {
|
|---|
| 130 | Node areaNode = coverageAreaNodes.item(j);
|
|---|
| 131 | Attribution attr = new Attribution();
|
|---|
| 132 | attr.attribution = attribution;
|
|---|
| 133 |
|
|---|
| 134 | attr.maxZoom = Integer.parseInt(zoomMaxXpath.evaluate(areaNode));
|
|---|
| 135 | attr.minZoom = Integer.parseInt(zoomMinXpath.evaluate(areaNode));
|
|---|
| 136 |
|
|---|
| 137 | Double southLat = Double.parseDouble(southLatXpath.evaluate(areaNode));
|
|---|
| 138 | Double northLat = Double.parseDouble(northLatXpath.evaluate(areaNode));
|
|---|
| 139 | Double westLon = Double.parseDouble(westLonXpath.evaluate(areaNode));
|
|---|
| 140 | Double eastLon = Double.parseDouble(eastLonXpath.evaluate(areaNode));
|
|---|
| 141 | attr.min = new Coordinate(southLat, westLon);
|
|---|
| 142 | attr.max = new Coordinate(northLat, eastLon);
|
|---|
| 143 |
|
|---|
| 144 | attributions.add(attr);
|
|---|
| 145 | }
|
|---|
| 146 | }
|
|---|
| 147 |
|
|---|
| 148 | return attributions;
|
|---|
| 149 | } catch (SAXException e) {
|
|---|
| 150 | System.err.println("Could not parse Bing aerials attribution metadata.");
|
|---|
| 151 | e.printStackTrace();
|
|---|
| 152 | } catch (ParserConfigurationException e) {
|
|---|
| 153 | e.printStackTrace();
|
|---|
| 154 | } catch (XPathExpressionException e) {
|
|---|
| 155 | e.printStackTrace();
|
|---|
| 156 | } catch (Exception e) {
|
|---|
| 157 | e.printStackTrace();
|
|---|
| 158 | }
|
|---|
| 159 |
|
|---|
| 160 | return null;
|
|---|
| 161 | }*/
|
|---|
| 162 |
|
|---|
| 163 | private List<Attribution> loadAttributionText() throws TileLoadException, IOException {
|
|---|
| 164 | List<Attribution> foundAttribs = new ArrayList<Attribution>();
|
|---|
| 165 |
|
|---|
| 166 | try {
|
|---|
| 167 | URL u = new URL("http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?include=ImageryProviders&output=xml&key="
|
|---|
| 168 | + API_KEY);
|
|---|
| 169 | URLConnection conn = u.openConnection();
|
|---|
| 170 |
|
|---|
| 171 | InputStream stream = conn.getInputStream();
|
|---|
| 172 |
|
|---|
| 173 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
|---|
| 174 | DocumentBuilder builder = factory.newDocumentBuilder();
|
|---|
| 175 | Document document = builder.parse(stream);
|
|---|
| 176 |
|
|---|
| 177 | Element eImagery = XMLTileParseUtil.getElement(document, "ImageryMetadata");
|
|---|
| 178 |
|
|---|
| 179 | Node nImageUrl=XMLTileParseUtil.getElement(document,eImagery, "ImageUrl");
|
|---|
| 180 | if (nImageUrl!=null) {
|
|---|
| 181 | imageUrlTemplate=XMLTileParseUtil.getNodeStrValue(nImageUrl);
|
|---|
| 182 | imageUrlTemplate = culturePattern.matcher(imageUrlTemplate).
|
|---|
| 183 | replaceAll(Locale.getDefault().toString());
|
|---|
| 184 | }
|
|---|
| 185 |
|
|---|
| 186 | Node nZoom=XMLTileParseUtil.getElement(document,eImagery,"ZoomMax");
|
|---|
| 187 | if (nZoom!=null)
|
|---|
| 188 | imageryZoomMax=XMLTileParseUtil.getNodeIntValue(nZoom);
|
|---|
| 189 |
|
|---|
| 190 | Element eSub=XMLTileParseUtil.getElement(document,eImagery,"ImageUrlSubdomains");
|
|---|
| 191 | if (eSub !=null){
|
|---|
| 192 | NodeList subdomainTxt = eSub.getElementsByTagName("string");
|
|---|
| 193 | subdomains = new String[subdomainTxt.getLength()];
|
|---|
| 194 |
|
|---|
| 195 | for(int i = 0; i < subdomainTxt.getLength(); i++)
|
|---|
| 196 | subdomains[i] = XMLTileParseUtil.getNodeStrValue(subdomainTxt.item(i));
|
|---|
| 197 | }
|
|---|
| 198 |
|
|---|
| 199 | NodeList imageryProviderNodes = eImagery.getElementsByTagName("ImageryProvider");
|
|---|
| 200 | foundAttribs = new ArrayList<Attribution>(imageryProviderNodes.getLength());
|
|---|
| 201 |
|
|---|
| 202 | for (int i = 0; i < imageryProviderNodes.getLength(); i++) {
|
|---|
| 203 | if (imageryProviderNodes.item(i).getNodeType() == Node.ELEMENT_NODE){
|
|---|
| 204 | String attribution = "";
|
|---|
| 205 | Element eProviderNode=(Element)imageryProviderNodes.item(i);
|
|---|
| 206 |
|
|---|
| 207 | Element eAttrib=XMLTileParseUtil.getElement(document,eProviderNode,"Attribution");
|
|---|
| 208 | if (eAttrib!=null)
|
|---|
| 209 | attribution=XMLTileParseUtil.getNodeStrValue(eAttrib);
|
|---|
| 210 |
|
|---|
| 211 | NodeList coverageAreaNodes = eProviderNode.getElementsByTagName("CoverageArea");
|
|---|
| 212 |
|
|---|
| 213 | for(int j = 0; j < coverageAreaNodes.getLength(); j++) {
|
|---|
| 214 | if (coverageAreaNodes.item(j).getNodeType() == Node.ELEMENT_NODE){
|
|---|
| 215 | Element eAreaNode = (Element)coverageAreaNodes.item(j);
|
|---|
| 216 | Attribution attr = new Attribution();
|
|---|
| 217 | attr.attribution = attribution;
|
|---|
| 218 |
|
|---|
| 219 | Node maxZoom=XMLTileParseUtil.getElement(document,eAreaNode,"ZoomMax");
|
|---|
| 220 | if (maxZoom!=null)
|
|---|
| 221 | attr.maxZoom = XMLTileParseUtil.getNodeIntValue(maxZoom);
|
|---|
| 222 |
|
|---|
| 223 | Node minZoom=XMLTileParseUtil.getElement(document,eAreaNode,"ZoomMin");
|
|---|
| 224 | if (minZoom!=null)
|
|---|
| 225 | attr.minZoom = XMLTileParseUtil.getNodeIntValue(minZoom);
|
|---|
| 226 |
|
|---|
| 227 | Node sLat=XMLTileParseUtil.getElement(document,eAreaNode,"SouthLatitude");
|
|---|
| 228 | Double dSouthLat = XMLTileParseUtil.getNodeDblValue(sLat);
|
|---|
| 229 |
|
|---|
| 230 | Node wLong=XMLTileParseUtil.getElement(document,eAreaNode,"WestLongitude");
|
|---|
| 231 | Double dWLong = XMLTileParseUtil.getNodeDblValue(wLong);
|
|---|
| 232 |
|
|---|
| 233 | Node northLat=XMLTileParseUtil.getElement(document,eAreaNode,"NorthLatitude");
|
|---|
| 234 | Double dNorthLat = XMLTileParseUtil.getNodeDblValue(northLat);
|
|---|
| 235 |
|
|---|
| 236 | Node eastLon=XMLTileParseUtil.getElement(document,eAreaNode,"EastLongitude");
|
|---|
| 237 | Double dEastLon = XMLTileParseUtil.getNodeDblValue(eastLon);
|
|---|
| 238 |
|
|---|
| 239 | if ((dSouthLat==null)||(dWLong==null) || (dNorthLat==null) || (dEastLon==null)) {
|
|---|
| 240 | TileLoadException e=new TileLoadException(
|
|---|
| 241 | "Unable to find proper tile " +
|
|---|
| 242 | "coordinates from source. Source " +
|
|---|
| 243 | "meta-data may have been modified.");
|
|---|
| 244 |
|
|---|
| 245 | throw e;
|
|---|
| 246 | }
|
|---|
| 247 |
|
|---|
| 248 | attr.min = new Coordinate(dSouthLat, dWLong);
|
|---|
| 249 | attr.max = new Coordinate(dNorthLat, dEastLon);
|
|---|
| 250 |
|
|---|
| 251 | foundAttribs.add(attr);
|
|---|
| 252 | }
|
|---|
| 253 | }
|
|---|
| 254 | }
|
|---|
| 255 | }
|
|---|
| 256 | } catch (SAXException e) {
|
|---|
| 257 | throw new TileLoadException("Could not parse Bing aerials attribution metadata.");
|
|---|
| 258 | } catch (ParserConfigurationException e) {
|
|---|
| 259 | throw new TileLoadException("Could not parse Bing aerials attribution metadata.");
|
|---|
| 260 | }
|
|---|
| 261 |
|
|---|
| 262 | return foundAttribs;
|
|---|
| 263 | }
|
|---|
| 264 |
|
|---|
| 265 | @Override
|
|---|
| 266 | public int getMaxZoom() {
|
|---|
| 267 | if(imageryZoomMax != null)
|
|---|
| 268 | return imageryZoomMax;
|
|---|
| 269 | else
|
|---|
| 270 | return 22;
|
|---|
| 271 | }
|
|---|
| 272 |
|
|---|
| 273 | public TileUpdate getTileUpdate() {
|
|---|
| 274 | return TileUpdate.IfNoneMatch;
|
|---|
| 275 | }
|
|---|
| 276 |
|
|---|
| 277 | @Override
|
|---|
| 278 | public boolean requiresAttribution() {
|
|---|
| 279 | return true;
|
|---|
| 280 | }
|
|---|
| 281 |
|
|---|
| 282 | @Override
|
|---|
| 283 | public Image getAttributionImage() {
|
|---|
| 284 | try {
|
|---|
| 285 | return ImageIO.read(getClass().getResourceAsStream("/org/openstreetmap/gui/jmapviewer/images/bing_maps.png"));
|
|---|
| 286 | } catch (IOException e) {
|
|---|
| 287 | return null;
|
|---|
| 288 | }
|
|---|
| 289 | }
|
|---|
| 290 |
|
|---|
| 291 | @Override
|
|---|
| 292 | public String getAttributionLinkURL() {
|
|---|
| 293 | //return "http://bing.com/maps"
|
|---|
| 294 | // FIXME: I've set attributionLinkURL temporarily to ToU URL to comply with bing ToU
|
|---|
| 295 | // (the requirement is that we have such a link at the bottom of the window)
|
|---|
| 296 | return "http://go.microsoft.com/?linkid=9710837";
|
|---|
| 297 | }
|
|---|
| 298 |
|
|---|
| 299 | @Override
|
|---|
| 300 | public String getTermsOfUseURL() {
|
|---|
| 301 | return "http://opengeodata.org/microsoft-imagery-details";
|
|---|
| 302 | }
|
|---|
| 303 |
|
|---|
| 304 | @Override
|
|---|
| 305 | public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) {
|
|---|
| 306 | try {
|
|---|
| 307 | if (!attributions.isDone())
|
|---|
| 308 | return "Loading Bing attribution data...";
|
|---|
| 309 | if (attributions.get() == null)
|
|---|
| 310 | return "Error loading Bing attribution data";
|
|---|
| 311 | StringBuilder a = new StringBuilder();
|
|---|
| 312 | for (Attribution attr : attributions.get()) {
|
|---|
| 313 | if (zoom <= attr.maxZoom && zoom >= attr.minZoom) {
|
|---|
| 314 | if (topLeft.getLon() < attr.max.getLon() && botRight.getLon() > attr.min.getLon()
|
|---|
| 315 | && topLeft.getLat() > attr.min.getLat() && botRight.getLat() < attr.max.getLat()) {
|
|---|
| 316 | a.append(attr.attribution);
|
|---|
| 317 | a.append(" ");
|
|---|
| 318 | }
|
|---|
| 319 | }
|
|---|
| 320 | }
|
|---|
| 321 | return a.toString();
|
|---|
| 322 | } catch (Exception e) {
|
|---|
| 323 | e.printStackTrace();
|
|---|
| 324 | }
|
|---|
| 325 | return "Error loading Bing attribution data";
|
|---|
| 326 | }
|
|---|
| 327 |
|
|---|
| 328 | static String computeQuadTree(int zoom, int tilex, int tiley) {
|
|---|
| 329 | StringBuilder k = new StringBuilder();
|
|---|
| 330 | for (int i = zoom; i > 0; i--) {
|
|---|
| 331 | char digit = 48;
|
|---|
| 332 | int mask = 1 << (i - 1);
|
|---|
| 333 | if ((tilex & mask) != 0) {
|
|---|
| 334 | digit += 1;
|
|---|
| 335 | }
|
|---|
| 336 | if ((tiley & mask) != 0) {
|
|---|
| 337 | digit += 2;
|
|---|
| 338 | }
|
|---|
| 339 | k.append(digit);
|
|---|
| 340 | }
|
|---|
| 341 | return k.toString();
|
|---|
| 342 | }
|
|---|
| 343 | }
|
|---|