import * as d3 from 'd3'; import _ from 'lodash'; import { rebind } from '../util/rebind'; import { t } from '../util/locale'; import { bindOnce } from '../util/bind_once'; import { getDimensions } from '../util/dimensions'; import { Areas, Labels, Layers, Lines, Midpoints, Points, Vertices } from '../svg/index'; import { Extent, interp } from '../geo/index'; import { fastMouse, setTransform, functor } from '../util/index'; import { flash } from '../ui/index'; export function Map(context) { var dimensions = [1, 1], dispatch = d3.dispatch('move', 'drawn'), projection = context.projection, dblclickEnabled = true, redrawEnabled = true, transformStart = { x: 0, y: 0, k: 1 }, 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; var zoom = d3.zoom() .scaleExtent([ztok(2), ztok(24)]) .on('zoom', zoomPan); var _selection = d3.select(null); function map(selection) { _selection = 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, d3.zoomIdentity .translate(projection.translate()) .scale(projection.scale()) ); 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.call('drawn', this, {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.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 = 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.call('drawn', this, {full: false}); } }); map.dimensions(getDimensions(selection)); drawLabels.supersurface(supersurface); } 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 = 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.call('drawn', this, {full: true}); } function editOff() { context.features().resetStats(); surface.selectAll('.layer-osm *').remove(); dispatch.call('drawn', this, {full: true}); } function dblClick() { if (!dblclickEnabled) { d3.event.preventDefault(); d3.event.stopImmediatePropagation(); } } function zoomPan(manualEvent) { var eventTransform = (manualEvent || d3.event).transform; if (ktoz(eventTransform.k * 2 * Math.PI) < minzoom) { surface.interrupt(); flash(context.container()) .select('.content') .text(t('cannot_zoom')); setZoom(context.minEditableZoom(), true); queueRedraw(); dispatch.call('move', this, map); return; } var t = projection.translate(), k = projection.scale(); if (t[0] === eventTransform.x && t[1] === eventTransform.y && k === eventTransform.k) { return; // no change } projection .translate([eventTransform.x, eventTransform.y]) .scale(eventTransform.k); var scale = eventTransform.k / transformStart.k, tX = (eventTransform.x / scale - transformStart.x) * scale, tY = (eventTransform.y / scale - transformStart.y) * scale; transformed = true; setTransform(supersurface, tX, tY, scale); queueRedraw(); dispatch.call('move', this, 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 = { k: projection.scale(), x: projection.translate()[0], y: projection.translate()[1] }; 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(_) { // TODO setZoom(_, true); // 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 k = Math.max(ztok(2), Math.min(ztok(24), ztok(_))) / (2 * Math.PI), center = pxCenter(), l = pointLocation(center); projection.scale(k); var t = projection.translate(); l = locationPoint(l); t[0] += center[0] - l[0]; t[1] += center[1] - l[1]; projection.translate(t); transformStart = { k: k, x: t[0], y: t[1] }; _selection.call(zoom.transform, d3.zoomIdentity.translate(t[0], t[1]).scale(k)); return true; } function setCenter(_) { var c = map.center(); if (_[0] === c[0] && _[1] === c[1]) return false; var t = projection.translate(), k = projection.scale(), pxC = pxCenter(), ll = projection(_); t[0] = t[0] - ll[0] + pxC[0]; t[1] = t[1] - ll[1] + pxC[1]; projection.translate(t); transformStart = { k: k, x: t[0], y: t[1] }; _selection.call(zoom.transform, d3.zoomIdentity.translate(t[0], t[1]).scale(k)); return true; } map.pan = function(d) { var t = projection.translate(), k = projection.scale(); t[0] += d[0]; t[1] += d[1]; projection.translate(t); transformStart = { k: k, x: t[0], y: t[1] }; _selection.call(zoom.transform, d3.zoomIdentity.translate(t[0], t[1]).scale(k)); dispatch.call('move', this, 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.call('move', this, map); } return redraw(); }; map.zoom = function(z) { if (!arguments.length) { return Math.max(ktoz(projection.scale() * 2 * Math.PI), 0); } if (z < minzoom) { surface.interrupt(); flash(context.container()) .select('.content') .text(t('cannot_zoom')); z = context.minEditableZoom(); } if (setZoom(z)) { dispatch.call('move', this, 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.call('move', this, map); } return redraw(); }; map.centerEase = function(loc2, duration) { duration = duration || 250; bindOnce(surface, 'mousedown.ease', function() { map.cancelEase(); }); if (easing) { map.cancelEase(); } var t1 = Date.now(), t2 = t1 + duration, loc1 = map.center(), ease = d3.easeCubicInOut; 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); return !easing; }); return map; }; map.cancelEase = function() { easing = false; d3.timerFlush(); 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 rebind(map, dispatch, 'on'); }