mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 17:23:02 +00:00
532 lines
16 KiB
JavaScript
532 lines
16 KiB
JavaScript
export function Map(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');
|
|
}
|