diff --git a/modules/renderer/background.js b/modules/renderer/background.js index dc2bb3d3e..fa2fe6c46 100644 --- a/modules/renderer/background.js +++ b/modules/renderer/background.js @@ -26,6 +26,15 @@ export function rendererBackground(context) { function background(selection) { + // If we are displaying an Esri basemap at high zoom, + // check its tilemap to see how high the zoom can go + if (context.map().zoom() > 18) { + var basemap = baseLayer.source(); + if (basemap && /^EsriWorldImagery/.test(basemap.id)) { + var center = context.map().center(); + basemap.fetchTilemap(center); + } + } var baseFilter = ''; if (detected.cssfilters) { @@ -114,16 +123,17 @@ export function rendererBackground(context) { background.updateImagery = function() { if (context.inIntro()) return; - var b = background.baseLayerSource(), - o = _overlayLayers - .filter(function (d) { return !d.source().isLocatorOverlay() && !d.source().isHidden(); }) - .map(function (d) { return d.source().id; }) - .join(','), - meters = geoOffsetToMeters(b.offset()), - epsilon = 0.01, - x = +meters[0].toFixed(2), - y = +meters[1].toFixed(2), - q = utilStringQs(window.location.hash.substring(1)); + var b = background.baseLayerSource(); + var o = _overlayLayers + .filter(function (d) { return !d.source().isLocatorOverlay() && !d.source().isHidden(); }) + .map(function (d) { return d.source().id; }) + .join(','); + + var meters = geoOffsetToMeters(b.offset()); + var epsilon = 0.01; + var x = +meters[0].toFixed(2); + var y = +meters[1].toFixed(2); + var q = utilStringQs(window.location.hash.substring(1)); var id = b.id; if (id === 'custom') { @@ -215,13 +225,12 @@ export function rendererBackground(context) { if (!osm) return background; var blacklists = context.connection().imageryBlacklists(); + var template = d.template(); + var fail = false; + var tested = 0; + var regex; - var template = d.template(), - fail = false, - tested = 0, - regex, i; - - for (i = 0; i < blacklists.length; i++) { + for (var i = 0; i < blacklists.length; i++) { try { regex = new RegExp(blacklists[i]); fail = regex.test(template); @@ -270,7 +279,6 @@ export function rendererBackground(context) { background.toggleOverlayLayer = function(d) { var layer; - for (var i = 0; i < _overlayLayers.length; i++) { layer = _overlayLayers[i]; if (layer.source() === d) { @@ -350,12 +358,12 @@ export function rendererBackground(context) { return geoExtent([args[2], args[1]]); } - var dataImagery = data.imagery || [], - q = utilStringQs(window.location.hash.substring(1)), - requested = q.background || q.layer, - extent = parseMap(q.map), - first, - best; + var dataImagery = data.imagery || []; + var q = utilStringQs(window.location.hash.substring(1)); + var requested = q.background || q.layer; + var extent = parseMap(q.map); + var first; + var best; // Add all the available imagery sources _backgroundSources = dataImagery.map(function(source) { diff --git a/modules/renderer/background_source.js b/modules/renderer/background_source.js index d608aae07..074796972 100644 --- a/modules/renderer/background_source.js +++ b/modules/renderer/background_source.js @@ -6,8 +6,16 @@ import { geoMercatorRaw as d3_geoMercatorRaw } from 'd3-geo'; +import { json as d3_json } from 'd3-request'; + import { t } from '../util/locale'; -import { geoExtent, geoPolygonIntersectsPolygon } from '../geo'; + +import { + geoExtent, + geoPolygonIntersectsPolygon, + geoSphericalDistance +} from '../geo'; + import { jsonpRequest } from '../util/jsonp_request'; import { utilDetect } from '../util/detect'; @@ -109,19 +117,20 @@ export function rendererBackgroundSource(data) { var lat = Math.atan(sinh(Math.PI * (1 - 2 * y / zoomSize))); switch (this.projection) { - case 'EPSG:4326': // todo: alternative codes of WGS 84? - return { - x: lon * 180 / Math.PI, - y: lat * 180 / Math.PI - }; - default: // EPSG:3857 and synonyms - var mercCoords = d3_geoMercatorRaw(lon, lat); - return { - x: 20037508.34 / Math.PI * mercCoords[0], - y: 20037508.34 / Math.PI * mercCoords[1] - }; + case 'EPSG:4326': // todo: alternative codes of WGS 84? + return { + x: lon * 180 / Math.PI, + y: lat * 180 / Math.PI + }; + default: // EPSG:3857 and synonyms + var mercCoords = d3_geoMercatorRaw(lon, lat); + return { + x: 20037508.34 / Math.PI * mercCoords[0], + y: 20037508.34 / Math.PI * mercCoords[1] + }; } }).bind(this); + var minXmaxY = tileToProjectedCoords(coord[0], coord[1], coord[2]); var maxXminY = tileToProjectedCoords(coord[0]+1, coord[1]+1, coord[2]); return template @@ -205,13 +214,13 @@ rendererBackgroundSource.Bing = function(data, dispatch) { data.template = 'https://ecn.t{switch:0,1,2,3}.tiles.virtualearth.net/tiles/a{u}.jpeg?g=587&mkt=en-gb&n=z'; - var bing = rendererBackgroundSource(data), - key = 'Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU', // Same as P2 and JOSM - url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?include=ImageryProviders&key=' + - key + '&jsonp={callback}', - cache = {}, - inflight = {}, - providers = []; + var bing = rendererBackgroundSource(data); + var key = 'Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU'; // Same as P2 and JOSM + var url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?include=ImageryProviders&key=' + + key + '&jsonp={callback}'; + var cache = {}; + var inflight = {}; + var providers = []; jsonpRequest(url, function(json) { providers = json.resourceSets[0].resources[0].imageryProviders.map(function(provider) { @@ -244,10 +253,10 @@ rendererBackgroundSource.Bing = function(data, dispatch) { bing.getMetadata = function(center, tileCoord, callback) { - var tileId = tileCoord.slice(0, 3).join('/'), - zoom = Math.min(tileCoord[2], 21), - centerPoint = center[1] + ',' + center[0], // lat,lng - url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial/' + centerPoint + + var tileId = tileCoord.slice(0, 3).join('/'); + var zoom = Math.min(tileCoord[2], 21); + var centerPoint = center[1] + ',' + center[0]; // lat,lng + var url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial/' + centerPoint + '?zl=' + zoom + '&key=' + key + '&jsonp={callback}'; if (inflight[tileId]) return; @@ -290,25 +299,57 @@ rendererBackgroundSource.Bing = function(data, dispatch) { rendererBackgroundSource.Esri = function(data) { + var esri = rendererBackgroundSource(data); + var cache = {}; + var inflight = {}; + var _prevCenter; - // don't request blank tiles, instead overzoom real tiles - #4327 - // deprecated technique, but it works (for now) - if (data.template.match(/blankTile/) === null) { - data.template = data.template + '?blankTile=false'; - } + // use a tilemap service to set maximum zoom for esri tiles dynamically + // https://developers.arcgis.com/documentation/tiled-elevation-service/ + esri.fetchTilemap = function(center) { + // skip if we have already fetched a tilemap within 5km + if (_prevCenter && geoSphericalDistance(center, _prevCenter) < 5000) return; + _prevCenter = center; - var esri = rendererBackgroundSource(data), - cache = {}, - inflight = {}; + // tiles are available globally to zoom level 19, afterward they may or may not be present + var z = 20; + + // first generate a random url using the template + var dummyUrl = esri.url([1,2,3]); + + // calculate url z/y/x from the lat/long of the center of the map + var x = (Math.floor((center[0] + 180) / 360 * Math.pow(2, z))); + var y = (Math.floor((1 - Math.log(Math.tan(center[1] * Math.PI / 180) + 1 / Math.cos(center[1] * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, z))); + + // fetch an 8x8 grid because responses to leverage cache + var tilemapUrl = dummyUrl.replace(/tile\/[0-9]+\/[0-9]+\/[0-9]+/, 'tilemap') + '/' + z + '/' + y + ' /' + x + '/8/8'; + + // make the request and introspect the response from the tilemap server + d3_json(tilemapUrl, function (err, tilemap) { + if (err || !tilemap) return; + + var hasTiles = true; + for (var i = 0; i < tilemap.data.length; i++) { + // 0 means an individual tile in the grid doesn't exist + if (!tilemap.data[i]) { + hasTiles = false; + break; + } + } + + // if any tiles are missing at level 20 we restrict maxZoom to 19 + esri.scaleExtent[1] = (hasTiles ? 20 : 19); + }); + }; esri.getMetadata = function(center, tileCoord, callback) { - var tileId = tileCoord.slice(0, 3).join('/'), - zoom = Math.min(tileCoord[2], esri.scaleExtent[1]), - centerPoint = center[0] + ',' + center[1], // long, lat (as it should be) - unknown = t('info_panels.background.unknown'), - metadataLayer, - vintage = {}, - metadata = {}; + var tileId = tileCoord.slice(0, 3).join('/'); + var zoom = Math.min(tileCoord[2], esri.scaleExtent[1]); + var centerPoint = center[0] + ',' + center[1]; // long, lat (as it should be) + var unknown = t('info_panels.background.unknown'); + var metadataLayer; + var vintage = {}; + var metadata = {}; if (inflight[tileId]) return;