<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <style>

    path {
      fill: none;
      stroke-width: 2px;
      stroke-linejoin: round;
    }

    .state {
      stroke: #999;
      stroke-width: 1px;
      fill: papayawhip;
    }

    .simplified {
      stroke: #de1e3d;
      stroke-width: 2px;
      stroke-dasharray: 8,8;
    }

    .zone {
      stroke: #0eb8ba;
    }

    .hidden {
      display: none;
    }

  </style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="warper.js"></script>
<script src="simplify.js"></script>
<script>

var stripWidth = 80;

var projection = d3.geo.conicConformal()
  .parallels([36, 37 + 15 / 60])
  .rotate([119, -35 - 20 / 60])
  .scale(3433)
  .translate([355, 498]);

var line = d3.svg.line();

// Top point
var origin = [50, 100];

d3.json("norway.geojson",function(err,ca){

  // Preproject to screen coords
  ca.coordinates[0] = ca.coordinates[0].map(projection);

  // Get coastline
  var ls = ca.coordinates[0].slice(0, 155);

  // Get simplified vertices
  var simplified = simplify(ls, 1000);

  var zones = d3.select("body").append("svg")
    .attr("width", 960)
    .attr("height", 720)
    .selectAll("g")
    .data(getZones(simplified))
    .enter()
      .append("g");

  zones.append("defs")
    .append("clipPath")
    .attr("id",function(d, i){
      return "clip" + i;
    })
    .append("path");

  var inner = zones.append("g")
    .attr("class",function(d, i) {
      return i ? "hidden" : null;
    });

  inner.append("path")
    .attr("class", "state");

  inner.append("line")
    .attr("class", "simplified fade hidden");

  // Put boundary outside so it isn't clipped
  zones.append("path")
    .attr("class", "zone fade hidden");

  zones.call(update);

  // Step-by-step for demo purposes
  d3.select("body")
    .transition()
    .duration(1000)
    .each("end", clipState)
    .transition()
    .each("end", showLine)
    .transition()
    .each("end", showZones)
    .transition()
    .each("end", move);

  // 1. Clip out the rest of CA
  function clipState() {
    inner.classed("hidden", false)
      .attr("clip-path",function(d, i){
        return "url(#clip" + i + ")";
      });
  }

  // 2. Show the simplified line
  function showLine() {
    inner.select(".simplified")
      .classed("hidden", false);
  }

  // 3. Show the zone boundaries
  function showZones() {
    zones.select(".zone")
      .classed("hidden", false);
  }

  // 4. Rotate/translate all the zones
  function move() {

    warpZones(zones.data());

    zones.transition()
      .duration(2000)
      .each("end",align)
      .call(update);

  }

  // 5. Warp the zones to rectangles
  function align(z) {

    z.project = function(d){
      return z.warp(z.translate(d));
    };

    z.boundary = z.corners;

    d3.select(this)
      .transition()
      .duration(750)
      .call(update)
      .each("end",fade);

  }

  // 6. Fade out
  function fade() {

    d3.select(this).selectAll(".fade")
      .transition()
      .duration(500)
      .style("opacity", 0);

  }

  // Redraw
  function update(sel) {

    sel.select(".zone")
      .attr("d",function(d){
        // TODO why does this not work?
        // return line(d.boundary);
        return line(d.boundary.slice(0,4)) + "Z";
      });

    sel.select(".state")
      .attr("d",function(d){
        return d.path(ca);
      });

    sel.select(".simplified")
      .attr("x1",function(d){
        return d.ends[0][0];
      })
      .attr("x2",function(d){
        return d.ends[1][0];
      })
      .attr("y1",function(d){
        return d.ends[0][1];
      })
      .attr("y2",function(d){
        return d.ends[1][1];
      });

    sel.select("clipPath path")
      .attr("d",function(d){
        return line(d.boundary.slice(0,4)) + "Z";
      });

  }

});

// Turn a simplified LineString into one group per segment
function getZones(simp) {

  return simp.slice(1).map(function(p, i){

    return {
      boundary: getBoundary(simp[i - 1], simp[i], p, simp[i + 2]),
      ends: [simp[i], p],
      path: d3.geo.path().projection(null)
    };

  });

}

function warpZones(zones) {

  zones.forEach(function(z,i){

    var angle = getAngle(z.ends[0], z.ends[1]),
        anchor = i ? zones[i - 1].ends[1] : origin;

    // Anchor points to end of prev segment
    var translate = [
      anchor[0] - z.ends[0][0],
      anchor[1] - z.ends[0][1]
    ];

    // Get translation/rotation function
    z.translate = translateAndRotate(translate, z.ends[0], angle);

    // Warp the boundary line and the simplified segment
    z.ends = z.ends.map(z.translate);
    z.boundary = z.boundary.map(z.translate);

    var top = bisect(null, z.ends[0], z.ends[1]),
        bottom = bisect(z.ends[0], z.ends[1], null);

    z.corners = [top[0], top[1], bottom[1], bottom[0], top[0]];

    z.corners.push(z.corners[0]);

    // See: http://bl.ocks.org/veltman/8f5a157276b1dc18ce2fba1bc06dfb48
    z.warp = warper(z.boundary, z.corners);

    z.project = function(d){
      return z.translate(d);
    };

    z.path.projection(d3.geo.transform({
      point: function(x, y) {
        var p = z.project([x, y]);
        this.stream.point(p[0], p[1]);
      }
    }));

  });

}

function getBoundary(prev, first, second, next) {

  // if prev is undefined, top is perpendicular through first
  // otherwise top bisects the prev-first-second angle
  // if next is undefined, bottom is perpendicular through second
  // otherwise bottom bisects the first-second-next angle
  var top = bisect(prev, first, second),
      bottom = bisect(first, second, next);

  return [top[0], top[1], bottom[1], bottom[0], top[0]];
}

function getAngle(a, b) {

  return Math.atan2(b[1] - a[1], b[0] - a[0]);

}

// Given an anchor point, initial translate, and angle rotation
// Return a function to translate+rotate a point
function translateAndRotate(translate, anchor, angle) {

  var cos = Math.cos(angle),
      sin = Math.sin(angle);

  return function(point) {

    return [
      translate[0] + anchor[0] + ( cos * (point[0] - anchor[0]) + sin * (point[1] - anchor[1])),
      translate[1] + anchor[1] + ( -sin * (point[0] - anchor[0]) + cos * (point[1] - anchor[1]))
    ];

  };

}

// Hacky angle bisector
function bisect(start, vertex, end) {

  var at,
      bt,
      adjusted,
      right,
      left;

  if (start) {
    at = getAngle(start, vertex);
  }

  if (end) {
    bt = getAngle(vertex, end);
  }

  if (!start) {
    at = bt;
  }

  if (!end) {
    bt = at;
  }

  adjusted = bt - at;

  if (adjusted <= -Math.PI) {
    adjusted = 2 * Math.PI + adjusted;
  } else if (adjusted > Math.PI) {
    adjusted = adjusted - 2 * Math.PI;
  }

  right = (adjusted - Math.PI) / 2;
  left = Math.PI + right;

  left += at;
  right += at;

  return [
    [vertex[0] + stripWidth * Math.cos(left) / 2, vertex[1] + stripWidth * Math.sin(left) / 2],
    [vertex[0] + stripWidth * Math.cos(right) / 2, vertex[1] + stripWidth * Math.sin(right) / 2]
  ];
}

function id(d) {
  return d;
}

d3.select(self.frameElement).style("height", "720px");

</script>
</body>
</html>