Files
iD/modules/renderer/map.js
2017-09-25 14:35:33 -04:00

794 lines
23 KiB
JavaScript

import _compact from 'lodash-es/compact';
import _map from 'lodash-es/map';
import _values from 'lodash-es/values';
import { set as d3_set } from 'd3-collection';
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { interpolate as d3_interpolate } from 'd3-interpolate';
import {
event as d3_event,
select as d3_select
} from 'd3-selection';
import {
zoom as d3_zoom,
zoomIdentity as d3_zoomIdentity
} from 'd3-zoom';
import { t } from '../util/locale';
import { geoExtent } from '../geo';
import {
modeBrowse,
modeSelect
} from '../modes';
import {
svgAreas,
svgLabels,
svgLayers,
svgLines,
svgMidpoints,
svgPoints,
svgVertices
} from '../svg';
import { uiFlash } from '../ui';
import {
utilFastMouse,
utilFunctor,
utilRebind,
utilSetTransform
} from '../util';
import { utilBindOnce } from '../util/bind_once';
import { utilGetDimensions } from '../util/dimensions';
export function rendererMap(context) {
var dimensions = [1, 1],
dispatch = d3_dispatch('move', 'drawn'),
projection = context.projection,
curtainProjection = context.curtainProjection,
dblclickEnabled = true,
redrawEnabled = true,
transformStart = projection.transform(),
transformLast,
transformed = false,
minzoom = 0,
drawLayers = svgLayers(projection, context),
drawPoints = svgPoints(projection, context),
drawVertices = svgVertices(projection, context),
drawLines = svgLines(projection, context),
drawAreas = svgAreas(projection, context),
drawMidpoints = svgMidpoints(projection, context),
drawLabels = svgLabels(projection, context),
supersurface = d3_select(null),
wrapper = d3_select(null),
surface = d3_select(null),
mouse,
mousemove;
var zoom = d3_zoom()
.scaleExtent([ztok(2), ztok(24)])
.interpolate(d3_interpolate)
.filter(zoomEventFilter)
.on('zoom', zoomPan);
var _selection = d3_select(null);
var isRedrawScheduled = false;
var pendingRedrawCall;
function scheduleRedraw() {
// Only schedule the redraw if one has not already been set.
if (isRedrawScheduled) return;
isRedrawScheduled = true;
var that = this;
var args = arguments;
pendingRedrawCall = requestIdleCallback(function () {
// Reset the boolean so future redraws can be set.
isRedrawScheduled = false;
redraw.apply(that, args);
}, { timeout: 1400 });
}
function cancelPendingRedraw() {
isRedrawScheduled = false;
window.cancelIdleCallback(pendingRedrawCall);
}
function map(selection) {
_selection = selection;
context
.on('change.map', immediateRedraw);
var osm = context.connection();
if (osm) {
osm.on('change.map', immediateRedraw);
}
context.history()
.on('change.map', immediateRedraw)
.on('undone.map redone.map', function(stack) {
var mode = context.mode().id;
if (mode !== 'browse' && mode !== 'select') return;
var followSelected = false;
if (Array.isArray(stack.selectedIDs)) {
followSelected = (stack.selectedIDs.length === 1 && stack.selectedIDs[0][0] === 'n');
context.enter(
modeSelect(context, stack.selectedIDs).follow(followSelected)
);
}
if (!followSelected && stack.transform) {
map.transformEase(stack.transform);
}
});
context.background()
.on('change.map', immediateRedraw);
context.features()
.on('redraw.map', immediateRedraw);
drawLayers
.on('change.map', function() {
context.background().updateImagery();
immediateRedraw();
});
selection
.on('dblclick.map', dblClick)
.call(zoom)
.call(zoom.transform, projection.transform());
supersurface = selection.append('div')
.attr('id', 'supersurface')
.call(utilSetTransform, 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
.call(drawLabels.observe)
.on('mousedown.zoom', function() {
if (d3_event.button === 2) {
d3_event.stopPropagation();
}
}, true)
.on('mouseup.zoom', function() {
if (resetTransform()) immediateRedraw();
})
.on('mousemove.map', function() {
mousemove = d3_event;
})
.on('mouseover.vertices', function() {
if (map.editable() && !transformed) {
var hover = d3_event.target.__data__;
surface.selectAll('.data-layer-osm')
.call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom());
dispatch.call('drawn', this, {full: false});
}
})
.on('mouseout.vertices', function() {
if (map.editable() && !transformed) {
var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__;
surface.selectAll('.data-layer-osm')
.call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom());
dispatch.call('drawn', this, {full: false});
}
});
supersurface
.call(context.background());
context.on('enter.map', function() {
if (map.editable() && !transformed) {
var all = context.intersects(map.extent()),
filter = utilFunctor(true),
graph = context.graph();
all = context.features().filter(all, graph);
surface.selectAll('.data-layer-osm')
.call(drawVertices, graph, all, filter, map.extent(), map.zoom())
.call(drawMidpoints, graph, all, filter, map.trimmedExtent());
dispatch.call('drawn', this, {full: false});
}
});
map.dimensions(utilGetDimensions(selection));
}
function zoomEventFilter() {
// Fix for #2151, (see also d3/d3-zoom#60, d3/d3-brush#18)
// Intercept `mousedown` and check if there is an orphaned zoom gesture.
// This can happen if a previous `mousedown` occurred without a `mouseup`.
// If we detect this, dispatch `mouseup` to complete the orphaned gesture,
// so that d3-zoom won't stop propagation of new `mousedown` events.
if (d3_event.type === 'mousedown') {
var hasOrphan = false;
var listeners = window.__on;
for (var i = 0; i < listeners.length; i++) {
var listener = listeners[i];
if (listener.name === 'zoom' && listener.type === 'mouseup') {
hasOrphan = true;
break;
}
}
if (hasOrphan) {
var event = window.CustomEvent;
if (event) {
event = new event('mouseup');
} else {
event = window.document.createEvent('Event');
event.initEvent('mouseup', false, false);
}
// Event needs to be dispatched with an event.view property.
event.view = window;
window.dispatchEvent(event);
}
}
return d3_event.button !== 2; // ignore right clicks
}
function ztok(z) {
return 256 * Math.pow(2, z);
}
function ktoz(k) {
return Math.max(Math.log(k) / Math.LN2 - 8, 0);
}
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 = utilFunctor(true);
}
}
data = features.filter(data, graph);
surface.selectAll('.data-layer-osm')
.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.call('drawn', this, {full: true});
}
function editOff() {
context.features().resetStats();
surface.selectAll('.layer-osm *').remove();
context.enter(modeBrowse(context));
dispatch.call('drawn', this, {full: true});
}
function dblClick() {
if (!dblclickEnabled) {
d3_event.preventDefault();
d3_event.stopImmediatePropagation();
}
}
function zoomPan(manualEvent) {
var event = (manualEvent || d3_event),
source = event.sourceEvent,
eventTransform = event.transform;
if (transformStart.x === eventTransform.x &&
transformStart.y === eventTransform.y &&
transformStart.k === eventTransform.k) {
return; // no change
}
// Normalize mousewheel - #3029
// If wheel delta is provided in LINE units, recalculate it in PIXEL units
// We are essentially redoing the calculations that occur here:
// https://github.com/d3/d3-zoom/blob/78563a8348aa4133b07cac92e2595c2227ca7cd7/src/zoom.js#L203
// See this for more info:
// https://github.com/basilfx/normalize-wheel/blob/master/src/normalizeWheel.js
if (source && source.type === 'wheel' && source.deltaMode === 1 /* LINE */) {
// pick sensible scroll amount if user scrolling fast or slow..
var lines = Math.abs(source.deltaY),
scroll = lines > 2 ? 40 : lines * 10;
var t0 = transformed ? transformLast : transformStart,
p0 = mouse(source),
p1 = t0.invert(p0),
k2 = t0.k * Math.pow(2, -source.deltaY * scroll / 500),
x2 = p0[0] - p1[0] * k2,
y2 = p0[1] - p1[1] * k2;
eventTransform = d3_zoomIdentity.translate(x2,y2).scale(k2);
_selection.node().__zoom = eventTransform;
}
if (ktoz(eventTransform.k * 2 * Math.PI) < minzoom) {
surface.interrupt();
uiFlash().text(t('cannot_zoom'));
setZoom(context.minEditableZoom(), true);
scheduleRedraw();
dispatch.call('move', this, map);
return;
}
projection.transform(eventTransform);
var scale = eventTransform.k / transformStart.k,
tX = (eventTransform.x / scale - transformStart.x) * scale,
tY = (eventTransform.y / scale - transformStart.y) * scale;
if (context.inIntro()) {
curtainProjection.transform({
x: eventTransform.x - tX,
y: eventTransform.y - tY,
k: eventTransform.k
});
}
mousemove = event;
transformed = true;
transformLast = eventTransform;
utilSetTransform(supersurface, tX, tY, scale);
scheduleRedraw();
dispatch.call('move', this, map);
}
function resetTransform() {
if (!transformed) return false;
// deprecation warning - Radial Menu to be removed in iD v3
surface.selectAll('.edit-menu, .radial-menu').interrupt().remove();
utilSetTransform(supersurface, 0, 0);
transformed = false;
if (context.inIntro()) {
curtainProjection.transform(projection.transform());
}
return true;
}
function redraw(difference, extent) {
if (surface.empty() || !redrawEnabled) return;
// 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 z = String(~~map.zoom());
if (surface.attr('data-zoom') !== z) {
surface.attr('data-zoom', z)
.classed('low-zoom', z <= 16);
}
if (!difference) {
supersurface.call(context.background());
}
wrapper
.call(drawLayers);
// OSM
if (map.editable()) {
context.loadTiles(projection, dimensions);
drawVector(difference, extent);
} else {
editOff();
}
transformStart = projection.transform();
return map;
}
var immediateRedraw = function(difference, extent) {
if (!difference && !extent) cancelPendingRedraw();
redraw(difference, extent);
};
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 event = mousemove || d3_event;
if (event) {
var s;
while ((s = event.sourceEvent)) { event = s; }
return mouse(event);
}
return null;
};
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 setTransform(t2, duration, force) {
var t = projection.transform();
if (!force && t2.k === t.k && t2.x === t.x && t2.y === t.y) {
return false;
}
if (duration) {
_selection
.transition()
.duration(duration)
.on('start', function() { map.startEase(); })
.call(zoom.transform, d3_zoomIdentity.translate(t2.x, t2.y).scale(t2.k));
} else {
projection.transform(t2);
transformStart = t2;
_selection.call(zoom.transform, transformStart);
}
}
function setZoom(z2, force, duration) {
if (z2 === map.zoom() && !force) {
return false;
}
var k = projection.scale(),
k2 = Math.max(ztok(2), Math.min(ztok(24), ztok(z2))) / (2 * Math.PI),
center = pxCenter(),
l = pointLocation(center);
projection.scale(k2);
var t = projection.translate();
l = locationPoint(l);
t[0] += center[0] - l[0];
t[1] += center[1] - l[1];
if (duration) {
projection.scale(k); // reset scale
_selection
.transition()
.duration(duration)
.on('start', function() { map.startEase(); })
.call(zoom.transform, d3_zoomIdentity.translate(t[0], t[1]).scale(k2));
} else {
projection.translate(t);
transformStart = projection.transform();
_selection.call(zoom.transform, transformStart);
}
return true;
}
function setCenter(loc2, duration) {
var c = map.center();
if (loc2[0] === c[0] && loc2[1] === c[1]) {
return false;
}
var t = projection.translate(),
k = projection.scale(),
pxC = pxCenter(),
ll = projection(loc2);
t[0] = t[0] - ll[0] + pxC[0];
t[1] = t[1] - ll[1] + pxC[1];
if (duration) {
_selection
.transition()
.duration(duration)
.on('start', function() { map.startEase(); })
.call(zoom.transform, d3_zoomIdentity.translate(t[0], t[1]).scale(k));
} else {
projection.translate(t);
transformStart = projection.transform();
_selection.call(zoom.transform, transformStart);
}
return true;
}
map.pan = function(delta, duration) {
var t = projection.translate(),
k = projection.scale();
t[0] += delta[0];
t[1] += delta[1];
if (duration) {
_selection
.transition()
.duration(duration)
.on('start', function() { map.startEase(); })
.call(zoom.transform, d3_zoomIdentity.translate(t[0], t[1]).scale(k));
} else {
projection.translate(t);
transformStart = projection.transform();
_selection.call(zoom.transform, transformStart);
dispatch.call('move', this, map);
immediateRedraw();
}
return map;
};
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 = utilFastMouse(supersurface.node());
setCenter(center);
scheduleRedraw();
return map;
};
function zoomIn(delta) {
setZoom(~~map.zoom() + delta, true, 250);
}
function zoomOut(delta) {
setZoom(~~map.zoom() - delta, true, 250);
}
map.zoomIn = function() { zoomIn(1); };
map.zoomInFurther = function() { zoomIn(4); };
map.zoomOut = function() { zoomOut(1); };
map.zoomOutFurther = function() { zoomOut(4); };
map.center = function(loc2) {
if (!arguments.length) {
return projection.invert(pxCenter());
}
if (setCenter(loc2)) {
dispatch.call('move', this, map);
}
scheduleRedraw();
return map;
};
map.zoom = function(z2) {
if (!arguments.length) {
return Math.max(ktoz(projection.scale() * 2 * Math.PI), 0);
}
if (z2 < minzoom) {
surface.interrupt();
uiFlash().text(t('cannot_zoom'));
z2 = context.minEditableZoom();
}
if (setZoom(z2)) {
dispatch.call('move', this, map);
}
scheduleRedraw();
return map;
};
map.zoomTo = function(entity, zoomLimits) {
var extent = entity.extent(context.graph());
if (!isFinite(extent.area())) return;
var z2 = map.trimmedExtentZoom(extent);
zoomLimits = zoomLimits || [context.minEditableZoom(), 20];
map.centerZoom(extent.center(), Math.min(Math.max(z2, zoomLimits[0]), zoomLimits[1]));
};
map.centerZoom = function(loc2, z2) {
var centered = setCenter(loc2),
zoomed = setZoom(z2);
if (centered || zoomed) {
dispatch.call('move', this, map);
}
scheduleRedraw();
return map;
};
map.centerEase = function(loc2, duration) {
duration = duration || 250;
setCenter(loc2, duration);
return map;
};
map.zoomEase = function(z2, duration) {
duration = duration || 250;
setZoom(z2, false, duration);
return map;
};
map.transformEase = function(t2, duration) {
duration = duration || 250;
setTransform(t2, duration, false);
return map;
};
map.startEase = function() {
utilBindOnce(surface, 'mousedown.ease', function() {
map.cancelEase();
});
return map;
};
map.cancelEase = function() {
_selection.interrupt();
return map;
};
map.extent = function(_) {
if (!arguments.length) {
return new geoExtent(projection.invert([0, dimensions[1]]),
projection.invert([dimensions[0], 0]));
} else {
var extent = geoExtent(_);
map.centerZoom(extent.center(), map.extentZoom(extent));
}
};
map.trimmedExtent = function(_) {
if (!arguments.length) {
var headerY = 60, footerY = 30, pad = 10;
return new geoExtent(projection.invert([pad, dimensions[1] - footerY - pad]),
projection.invert([dimensions[0] - pad, headerY + pad]));
} else {
var extent = geoExtent(_);
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(geoExtent(_), dimensions);
};
map.trimmedExtentZoom = function(_) {
var trimY = 120, trimX = 40,
trimmed = [dimensions[0] - trimX, dimensions[1] - trimY];
return calcZoom(geoExtent(_), trimmed);
};
map.editable = function() {
var osmLayer = surface.selectAll('.data-layer-osm');
if (!osmLayer.empty() && osmLayer.classed('disabled')) return false;
return map.zoom() >= context.minEditableZoom();
};
map.minzoom = function(_) {
if (!arguments.length) return minzoom;
minzoom = _;
return map;
};
map.layers = drawLayers;
return utilRebind(map, dispatch, 'on');
}