Files
iD/modules/renderer/tile_layer.js

312 lines
9.3 KiB
JavaScript

import { select as d3_select } from 'd3-selection';
import { t } from '../core/localizer';
import { geoScaleToZoom, geoVecLength } from '../geo';
import { utilPrefixCSSProperty, utilTiler } from '../util';
export function rendererTileLayer(context) {
var transformProp = utilPrefixCSSProperty('Transform');
var tiler = utilTiler();
var _tileSize = 256;
var _projection;
var _cache = {};
var _tileOrigin;
var _zoom;
var _source;
var _epsilon = 0;
// Workaround to remove visible grid around tile borders on Chrome with dynamic epsilon for specific browser zoom levels
// Should be removed when https://issues.chromium.org/issues/40084005 is resolved
// See https://github.com/openstreetmap/iD/pull/10594
if (window.chrome) {
updateEpsilon();
window.addEventListener('resize', updateEpsilon);
}
function updateEpsilon() {
const pageZoom = Math.round(window.devicePixelRatio * 100);
if (pageZoom % 25 === 0) {
_epsilon = 0; // uses mix-blend-mode: plus-lighter
} else if (pageZoom === 90) {
_epsilon = 0.005;
} else if (pageZoom === 110) {
_epsilon = 0.002;
} else {
_epsilon = 0.003;
}
}
function tileSizeAtZoom(d, z) {
return ((d.tileSize * Math.pow(2, z - d[2])) / d.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.url = _source.url(d);
d.tileSize = _tileSize;
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]
];
tiler
.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)) {
tiler.skipNullIsland(!!_source.overlay);
tiler().forEach(function(d) {
addSource(d);
if (d.url === '') return;
if (typeof d.url !== 'string') return; // Workaround for #2295
requests.push(d);
if (_cache[d.url] === false && lookUp(d)) {
requests.push(addSource(lookUp(d)));
}
});
requests = uniqueBy(requests, 'url').filter(function(r) {
// don't re-request tiles which have failed in the past
return _cache[r.url] !== false;
});
}
function load(d3_event, d) {
_cache[d.url] = true;
d3_select(this)
.on('error', null)
.on('load', null);
render(selection);
}
function error(d3_event, d) {
_cache[d.url] = false;
d3_select(this)
.on('error', null)
.on('load', null)
.remove();
render(selection);
}
function imageTransform(d) {
var ts = d.tileSize * Math.pow(2, _zoom - d[2]);
var scale = tileSizeAtZoom(d, _zoom);
return 'translate(' +
((d[0] * ts) * _tileSize / d.tileSize - _tileOrigin[0]
) + 'px,' +
((d[1] * ts) * _tileSize / d.tileSize - _tileOrigin[1]
) + 'px) ' +
'scale(' + scale * _tileSize / d.tileSize + ',' + scale * _tileSize / d.tileSize + ')';
}
function tileCenter(d) {
var ts = d.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 = tiler.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.url; });
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();
}
}, 250);
});
image.enter()
.append('img')
.attr('class', 'tile')
.attr('alt', '')
.attr('draggable', 'false')
.style('width', _tileSize + 'px')
.style('height', _tileSize + 'px')
.attr('src', function(d) { return d.url; })
.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.url; });
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) {
if (result && result.vintage && result.vintage.range) {
span.text(result.vintage.range);
} else {
span.text('');
span.call(t.append('info_panels.background.vintage'));
span.append('span').text(': ');
span.call(t.append('info_panels.background.unknown'));
}
});
});
}
}
background.projection = function(val) {
if (!arguments.length) return _projection;
_projection = val;
return background;
};
background.dimensions = function(val) {
if (!arguments.length) return tiler.size();
tiler.size(val);
return background;
};
background.source = function(val) {
if (!arguments.length) return _source;
_source = val;
_tileSize = _source.tileSize;
_cache = {};
tiler.tileSize(_source.tileSize).zoomExtent(_source.zoomExtent);
return background;
};
return background;
}