import { t } from '../util/locale'; import _ from 'lodash'; import { Areas, Labels, Layers, Lines, Midpoints, Points, Vertices } from '../svg/index'; import { Extent, interp } from '../geo/index'; import { fastMouse, setTransform } from '../util/index'; import { flash } from '../ui/index'; 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 = Layers(projection, context), drawPoints = Points(projection, context), drawVertices = Vertices(projection, context), drawLines = Lines(projection), drawAreas = Areas(projection), drawMidpoints = Midpoints(projection, context), drawLabels = 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(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(); 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; setTransform(supersurface, tX, tY, scale); queueRedraw(); dispatch.move(map); } function resetTransform() { if (!transformed) return false; surface.selectAll('.radial-menu').interrupt().remove(); 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 = 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(); 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 = 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 Extent(projection.invert([0, dimensions[1]]), projection.invert([dimensions[0], 0])); } else { var extent = Extent(_); map.centerZoom(extent.center(), map.extentZoom(extent)); } }; map.trimmedExtent = function(_) { if (!arguments.length) { var headerY = 60, footerY = 30, pad = 10; return new Extent(projection.invert([pad, dimensions[1] - footerY - pad]), projection.invert([dimensions[0] - pad, headerY + pad])); } else { var extent = 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(Extent(_), dimensions); }; map.trimmedExtentZoom = function(_) { var trimY = 120, trimX = 40, trimmed = [dimensions[0] - trimX, dimensions[1] - trimY]; return calcZoom(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'); }