text: import { setTransform, fastMouse } from '../util/index';
  import { interp, Extent } from '../geo/index';
  import { Layers, Areas, Midpoints, Points, Vertices, Lines, Labels } from '../svg/index';
  import { flash } from '../ui/core/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');
  }
  