Files
iD/js/id/renderer/map.js
Bryan Housel e6c440cfae Remove clearTimeout from queueRedraw, for long mousedrags or ease transitions
Old behavior would redraw only after the map stops moving for 300ms
New behavior will force redraw within 750ms, regardless of map stability
2016-05-27 16:02:21 -04:00

532 lines
16 KiB
JavaScript

iD.Map = function(context) {
var dimensions = [1, 1],
dispatch = d3.dispatch('move', 'drawn'),
projection = context.projection,
zoom = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale() * 2 * Math.PI)
.scaleExtent([1024, 256 * Math.pow(2, 24)])
.on('zoom', zoomPan),
dblclickEnabled = true,
redrawEnabled = true,
transformStart,
transformed = false,
easing = false,
minzoom = 0,
drawLayers = iD.svg.Layers(projection, context),
drawPoints = iD.svg.Points(projection, context),
drawVertices = iD.svg.Vertices(projection, context),
drawLines = iD.svg.Lines(projection),
drawAreas = iD.svg.Areas(projection),
drawMidpoints = iD.svg.Midpoints(projection, context),
drawLabels = iD.svg.Labels(projection, context),
supersurface,
wrapper,
surface,
mouse,
mousemove;
function map(selection) {
context
.on('change.map', redraw);
context.history()
.on('change.map', redraw);
context.background()
.on('change.map', redraw);
context.features()
.on('redraw.map', redraw);
drawLayers
.on('change.map', function() {
context.background().updateImagery();
redraw();
});
selection
.on('dblclick.map', dblClick)
.call(zoom);
supersurface = selection.append('div')
.attr('id', 'supersurface')
.call(iD.util.setTransform, 0, 0);
// Need a wrapper div because Opera can't cope with an absolutely positioned
// SVG element: http://bl.ocks.org/jfirebaugh/6fbfbd922552bf776c16
wrapper = supersurface
.append('div')
.attr('class', 'layer layer-data');
map.surface = surface = wrapper
.call(drawLayers)
.selectAll('.surface')
.attr('id', 'surface');
surface
.on('mousedown.zoom', function() {
if (d3.event.button === 2) {
d3.event.stopPropagation();
}
}, true)
.on('mouseup.zoom', function() {
if (resetTransform()) redraw();
})
.on('mousemove.map', function() {
mousemove = d3.event;
})
.on('mouseover.vertices', function() {
if (map.editable() && !transformed) {
var hover = d3.event.target.__data__;
surface.call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom());
dispatch.drawn({full: false});
}
})
.on('mouseout.vertices', function() {
if (map.editable() && !transformed) {
var hover = d3.event.relatedTarget && d3.event.relatedTarget.__data__;
surface.call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom());
dispatch.drawn({full: false});
}
});
supersurface
.call(context.background());
context.on('enter.map', function() {
if (map.editable() && !transformed) {
var all = context.intersects(map.extent()),
filter = d3.functor(true),
graph = context.graph();
all = context.features().filter(all, graph);
surface
.call(drawVertices, graph, all, filter, map.extent(), map.zoom())
.call(drawMidpoints, graph, all, filter, map.trimmedExtent());
dispatch.drawn({full: false});
}
});
map.dimensions(selection.dimensions());
drawLabels.supersurface(supersurface);
}
function pxCenter() {
return [dimensions[0] / 2, dimensions[1] / 2];
}
function drawVector(difference, extent) {
var graph = context.graph(),
features = context.features(),
all = context.intersects(map.extent()),
data, filter;
if (difference) {
var complete = difference.complete(map.extent());
data = _.compact(_.values(complete));
filter = function(d) { return d.id in complete; };
features.clear(data);
} else {
// force a full redraw if gatherStats detects that a feature
// should be auto-hidden (e.g. points or buildings)..
if (features.gatherStats(all, graph, dimensions)) {
extent = undefined;
}
if (extent) {
data = context.intersects(map.extent().intersection(extent));
var set = d3.set(_.map(data, 'id'));
filter = function(d) { return set.has(d.id); };
} else {
data = all;
filter = d3.functor(true);
}
}
data = features.filter(data, graph);
surface
.call(drawVertices, graph, data, filter, map.extent(), map.zoom())
.call(drawLines, graph, data, filter)
.call(drawAreas, graph, data, filter)
.call(drawMidpoints, graph, data, filter, map.trimmedExtent())
.call(drawLabels, graph, data, filter, dimensions, !difference && !extent)
.call(drawPoints, graph, data, filter);
dispatch.drawn({full: true});
}
function editOff() {
context.features().resetStats();
surface.selectAll('.layer-osm *').remove();
dispatch.drawn({full: true});
}
function dblClick() {
if (!dblclickEnabled) {
d3.event.preventDefault();
d3.event.stopImmediatePropagation();
}
}
function zoomPan() {
if (Math.log(d3.event.scale) / Math.LN2 - 8 < minzoom) {
surface.interrupt();
iD.ui.flash(context.container())
.select('.content')
.text(t('cannot_zoom'));
setZoom(context.minEditableZoom(), true);
queueRedraw();
dispatch.move(map);
return;
}
projection
.translate(d3.event.translate)
.scale(d3.event.scale / (2 * Math.PI));
var scale = d3.event.scale / transformStart[0],
tX = (d3.event.translate[0] / scale - transformStart[1][0]) * scale,
tY = (d3.event.translate[1] / scale - transformStart[1][1]) * scale;
transformed = true;
iD.util.setTransform(supersurface, tX, tY, scale);
queueRedraw();
dispatch.move(map);
}
function resetTransform() {
if (!transformed) return false;
surface.selectAll('.radial-menu').interrupt().remove();
iD.util.setTransform(supersurface, 0, 0);
transformed = false;
return true;
}
function redraw(difference, extent) {
if (!surface || !redrawEnabled) return;
clearTimeout(timeoutId);
// If we are in the middle of a zoom/pan, we can't do differenced redraws.
// It would result in artifacts where differenced entities are redrawn with
// one transform and unchanged entities with another.
if (resetTransform()) {
difference = extent = undefined;
}
var zoom = String(~~map.zoom());
if (surface.attr('data-zoom') !== zoom) {
surface.attr('data-zoom', zoom)
.classed('low-zoom', zoom <= 16);
}
if (!difference) {
supersurface.call(context.background());
}
// OSM
if (map.editable()) {
context.loadTiles(projection, dimensions);
drawVector(difference, extent);
} else {
editOff();
}
wrapper
.call(drawLayers);
transformStart = [
projection.scale() * 2 * Math.PI,
projection.translate().slice()];
return map;
}
var timeoutId;
function queueRedraw() {
timeoutId = setTimeout(function() { redraw(); }, 750);
}
function pointLocation(p) {
var translate = projection.translate(),
scale = projection.scale() * 2 * Math.PI;
return [(p[0] - translate[0]) / scale, (p[1] - translate[1]) / scale];
}
function locationPoint(l) {
var translate = projection.translate(),
scale = projection.scale() * 2 * Math.PI;
return [l[0] * scale + translate[0], l[1] * scale + translate[1]];
}
map.mouse = function() {
var e = mousemove || d3.event, s;
while ((s = e.sourceEvent)) e = s;
return mouse(e);
};
map.mouseCoordinates = function() {
return projection.invert(map.mouse());
};
map.dblclickEnable = function(_) {
if (!arguments.length) return dblclickEnabled;
dblclickEnabled = _;
return map;
};
map.redrawEnable = function(_) {
if (!arguments.length) return redrawEnabled;
redrawEnabled = _;
return map;
};
function interpolateZoom(_) {
var k = projection.scale(),
t = projection.translate();
surface.node().__chart__ = {
x: t[0],
y: t[1],
k: k * 2 * Math.PI
};
setZoom(_);
projection.scale(k).translate(t); // undo setZoom projection changes
zoom.event(surface.transition());
}
function setZoom(_, force) {
if (_ === map.zoom() && !force)
return false;
var scale = 256 * Math.pow(2, _),
center = pxCenter(),
l = pointLocation(center);
scale = Math.max(1024, Math.min(256 * Math.pow(2, 24), scale));
projection.scale(scale / (2 * Math.PI));
zoom.scale(scale);
var t = projection.translate();
l = locationPoint(l);
t[0] += center[0] - l[0];
t[1] += center[1] - l[1];
projection.translate(t);
zoom.translate(projection.translate());
return true;
}
function setCenter(_) {
var c = map.center();
if (_[0] === c[0] && _[1] === c[1])
return false;
var t = projection.translate(),
pxC = pxCenter(),
ll = projection(_);
projection.translate([
t[0] - ll[0] + pxC[0],
t[1] - ll[1] + pxC[1]]);
zoom.translate(projection.translate());
return true;
}
map.pan = function(d) {
var t = projection.translate();
t[0] += d[0];
t[1] += d[1];
projection.translate(t);
zoom.translate(projection.translate());
dispatch.move(map);
return redraw();
};
map.dimensions = function(_) {
if (!arguments.length) return dimensions;
var center = map.center();
dimensions = _;
drawLayers.dimensions(dimensions);
context.background().dimensions(dimensions);
projection.clipExtent([[0, 0], dimensions]);
mouse = iD.util.fastMouse(supersurface.node());
setCenter(center);
return redraw();
};
function zoomIn(integer) {
interpolateZoom(~~map.zoom() + integer);
}
function zoomOut(integer) {
interpolateZoom(~~map.zoom() - integer);
}
map.zoomIn = function() { zoomIn(1); };
map.zoomInFurther = function() { zoomIn(4); };
map.zoomOut = function() { zoomOut(1); };
map.zoomOutFurther = function() { zoomOut(4); };
map.center = function(loc) {
if (!arguments.length) {
return projection.invert(pxCenter());
}
if (setCenter(loc)) {
dispatch.move(map);
}
return redraw();
};
map.zoom = function(z) {
if (!arguments.length) {
return Math.max(Math.log(projection.scale() * 2 * Math.PI) / Math.LN2 - 8, 0);
}
if (z < minzoom) {
surface.interrupt();
iD.ui.flash(context.container())
.select('.content')
.text(t('cannot_zoom'));
z = context.minEditableZoom();
}
if (setZoom(z)) {
dispatch.move(map);
}
return redraw();
};
map.zoomTo = function(entity, zoomLimits) {
var extent = entity.extent(context.graph());
if (!isFinite(extent.area())) return;
var zoom = map.trimmedExtentZoom(extent);
zoomLimits = zoomLimits || [context.minEditableZoom(), 20];
map.centerZoom(extent.center(), Math.min(Math.max(zoom, zoomLimits[0]), zoomLimits[1]));
};
map.centerZoom = function(loc, z) {
var centered = setCenter(loc),
zoomed = setZoom(z);
if (centered || zoomed) {
dispatch.move(map);
}
return redraw();
};
map.centerEase = function(loc2, duration) {
duration = duration || 250;
surface.one('mousedown.ease', function() {
map.cancelEase();
});
if (easing) {
map.cancelEase();
}
var t1 = Date.now(),
t2 = t1 + duration,
loc1 = map.center(),
ease = d3.ease('cubic-in-out');
easing = true;
d3.timer(function() {
if (!easing) return true; // cancelled ease
var tNow = Date.now();
if (tNow > t2) {
tNow = t2;
easing = false;
}
var locNow = iD.geo.interp(loc1, loc2, ease((tNow - t1) / duration));
setCenter(locNow);
d3.event = {
scale: zoom.scale(),
translate: zoom.translate()
};
zoomPan();
return !easing;
});
return map;
};
map.cancelEase = function() {
easing = false;
d3.timer.flush();
return map;
};
map.extent = function(_) {
if (!arguments.length) {
return new iD.geo.Extent(projection.invert([0, dimensions[1]]),
projection.invert([dimensions[0], 0]));
} else {
var extent = iD.geo.Extent(_);
map.centerZoom(extent.center(), map.extentZoom(extent));
}
};
map.trimmedExtent = function(_) {
if (!arguments.length) {
var headerY = 60, footerY = 30, pad = 10;
return new iD.geo.Extent(projection.invert([pad, dimensions[1] - footerY - pad]),
projection.invert([dimensions[0] - pad, headerY + pad]));
} else {
var extent = iD.geo.Extent(_);
map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));
}
};
function calcZoom(extent, dim) {
var tl = projection([extent[0][0], extent[1][1]]),
br = projection([extent[1][0], extent[0][1]]);
// Calculate maximum zoom that fits extent
var hFactor = (br[0] - tl[0]) / dim[0],
vFactor = (br[1] - tl[1]) / dim[1],
hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2,
vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2,
newZoom = map.zoom() - Math.max(hZoomDiff, vZoomDiff);
return newZoom;
}
map.extentZoom = function(_) {
return calcZoom(iD.geo.Extent(_), dimensions);
};
map.trimmedExtentZoom = function(_) {
var trimY = 120, trimX = 40,
trimmed = [dimensions[0] - trimX, dimensions[1] - trimY];
return calcZoom(iD.geo.Extent(_), trimmed);
};
map.editable = function() {
return map.zoom() >= context.minEditableZoom();
};
map.minzoom = function(_) {
if (!arguments.length) return minzoom;
minzoom = _;
return map;
};
map.layers = drawLayers;
return d3.rebind(map, dispatch, 'on');
};