import { select as d3_select } from 'd3-selection'; import { t } from '../util/locale'; import { geoScaleToZoom, geoVecLength } from '../geo'; import { utilPrefixCSSProperty, utilTiler } from '../util'; export function rendererTileLayer(context) { var tileSize = 256; var transformProp = utilPrefixCSSProperty('Transform'); var geotile = utilTiler(); var _projection; var _cache = {}; var _tileOrigin; var _zoom; var _source; // blacklist overlay tiles around Null Island.. function nearNullIsland(x, y, z) { if (z >= 7) { var center = Math.pow(2, z - 1); var width = Math.pow(2, z - 6); var min = center - (width / 2); var max = center + (width / 2) - 1; return x >= min && x <= max && y >= min && y <= max; } return false; } function tileSizeAtZoom(d, z) { var EPSILON = 0.002; return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + EPSILON; } function atZoom(t, distance) { var power = Math.pow(2, distance); return [ Math.floor(t[0] * power), Math.floor(t[1] * power), t[2] + distance ]; } function lookUp(d) { for (var up = -1; up > -d[2]; up--) { var tile = atZoom(d, up); if (_cache[_source.url(tile)] !== false) { return tile; } } } function uniqueBy(a, n) { var o = []; var seen = {}; for (var i = 0; i < a.length; i++) { if (seen[a[i][n]] === undefined) { o.push(a[i]); seen[a[i][n]] = true; } } return o; } function addSource(d) { d.push(_source.url(d)); return d; } // Update tiles based on current state of `projection`. function background(selection) { _zoom = geoScaleToZoom(_projection.scale(), tileSize); var pixelOffset; if (_source) { pixelOffset = [ _source.offset()[0] * Math.pow(2, _zoom), _source.offset()[1] * Math.pow(2, _zoom) ]; } else { pixelOffset = [0, 0]; } var translate = [ _projection.translate()[0] + pixelOffset[0], _projection.translate()[1] + pixelOffset[1] ]; geotile .scale(_projection.scale() * 2 * Math.PI) .translate(translate); _tileOrigin = [ _projection.scale() * Math.PI - translate[0], _projection.scale() * Math.PI - translate[1] ]; render(selection); } // Derive the tiles onscreen, remove those offscreen and position them. // Important that this part not depend on `_projection` because it's // rentered when tiles load/error (see #644). function render(selection) { if (!_source) return; var requests = []; var showDebug = context.getDebug('tile') && !_source.overlay; if (_source.validZoom(_zoom)) { geotile().forEach(function(d) { addSource(d); if (d[3] === '') return; if (typeof d[3] !== 'string') return; // Workaround for #2295 requests.push(d); if (_cache[d[3]] === false && lookUp(d)) { requests.push(addSource(lookUp(d))); } }); requests = uniqueBy(requests, 3).filter(function(r) { if (!!_source.overlay && nearNullIsland(r[0], r[1], r[2])) { return false; } // don't re-request tiles which have failed in the past return _cache[r[3]] !== false; }); } function load(d) { _cache[d[3]] = true; d3_select(this) .on('error', null) .on('load', null) .classed('tile-loaded', true); render(selection); } function error(d) { _cache[d[3]] = false; d3_select(this) .on('error', null) .on('load', null) .remove(); render(selection); } function imageTransform(d) { var ts = tileSize * Math.pow(2, _zoom - d[2]); var scale = tileSizeAtZoom(d, _zoom); return 'translate(' + ((d[0] * ts) - _tileOrigin[0]) + 'px,' + ((d[1] * ts) - _tileOrigin[1]) + 'px) ' + 'scale(' + scale + ',' + scale + ')'; } function tileCenter(d) { var ts = tileSize * Math.pow(2, _zoom - d[2]); return [ ((d[0] * ts) - _tileOrigin[0] + (ts / 2)), ((d[1] * ts) - _tileOrigin[1] + (ts / 2)) ]; } function debugTransform(d) { var coord = tileCenter(d); return 'translate(' + coord[0] + 'px,' + coord[1] + 'px)'; } // Pick a representative tile near the center of the viewport // (This is useful for sampling the imagery vintage) var dims = geotile.size(); var mapCenter = [dims[0] / 2, dims[1] / 2]; var minDist = Math.max(dims[0], dims[1]); var nearCenter; requests.forEach(function(d) { var c = tileCenter(d); var dist = geoVecLength(c, mapCenter); if (dist < minDist) { minDist = dist; nearCenter = d; } }); var image = selection.selectAll('img') .data(requests, function(d) { return d[3]; }); image.exit() .style(transformProp, imageTransform) .classed('tile-removing', true) .classed('tile-center', false) .each(function() { var tile = d3_select(this); window.setTimeout(function() { if (tile.classed('tile-removing')) { tile.remove(); } }, 300); }); image.enter() .append('img') .attr('class', 'tile') .attr('src', function(d) { return d[3]; }) .on('error', error) .on('load', load) .merge(image) .style(transformProp, imageTransform) .classed('tile-debug', showDebug) .classed('tile-removing', false) .classed('tile-center', function(d) { return d === nearCenter; }); var debug = selection.selectAll('.tile-label-debug') .data(showDebug ? requests : [], function(d) { return d[3]; }); debug.exit() .remove(); if (showDebug) { var debugEnter = debug.enter() .append('div') .attr('class', 'tile-label-debug'); debugEnter .append('div') .attr('class', 'tile-label-debug-coord'); debugEnter .append('div') .attr('class', 'tile-label-debug-vintage'); debug = debug.merge(debugEnter); debug .style(transformProp, debugTransform); debug .selectAll('.tile-label-debug-coord') .text(function(d) { return d[2] + ' / ' + d[0] + ' / ' + d[1]; }); debug .selectAll('.tile-label-debug-vintage') .each(function(d) { var span = d3_select(this); var center = context.projection.invert(tileCenter(d)); _source.getMetadata(center, d, function(err, result) { span.text((result && result.vintage && result.vintage.range) || t('info_panels.background.vintage') + ': ' + t('info_panels.background.unknown') ); }); }); } } background.projection = function(_) { if (!arguments.length) return _projection; _projection = _; return background; }; background.dimensions = function(_) { if (!arguments.length) return geotile.size(); geotile.size(_); return background; }; background.source = function(_) { if (!arguments.length) return _source; _source = _; _cache = {}; geotile.scaleExtent(_source.scaleExtent); return background; }; return background; }