地球を12周する螺旋状平行四辺形（D3.js 利用、正距円筒図法）

こんにちは。
D3.js を使い、[正距円筒図法](https://ja.wikipedia.org/wiki/正距円筒図法) (equirectangular projection) 上で地球を12周する螺旋状平行四辺形を描いてみました（こちらが[Demonstration](http://bl.ocks.org/kkdd/b924dd3b9be643ded350ff629210bcaf)）。これは
「[Spiral Stress Test](https://www.jasondavies.com/maps/spiral/)」とほぼ同じですが、マウス（ホイール）を操作して下記の分割数を変更でき、また自動で回転もさせています。

[GeoJSON](https://en.wikipedia.org/wiki/GeoJSON) (RFC7946) では、線分（直線）とはこの正距円筒図法上での線分（直線）を意味します（＝ latitude-following curve）。しかし D3.js では地球の大圏弧を意味するので、D3.js を使って今回のような GeoJSON でいうところの直線（今回の平行四辺形の辺）を描くには凹凸が目立たなくなるまで十分に細かく分割する必要があります。

<img width="613" alt="spiral.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/54617/59dbf41b-f5e2-6fd3-f701-11c29f1ff220.jpeg">

```equirectangular_spiral.html
<!DOCTYPE html>
<meta charset = "utf-8" >
<title>equirectangular projection</title>
<style>
.land {
  fill: #eee;
  stroke: #888;
  stroke-width: .5px;
}

.boundary {
  fill: none;
  stroke: #ccc;
}

.spiral {
  fill: red;
  fill-opacity: .2;
  stroke: #f00;
  stroke-width: .5px;
}

.graticule {
  fill: none;
  stroke: #777;
  stroke-width: 0.3px;
  stroke-opacity: 0.5;
}
</style>
<body>
<script src= "https://d3js.org/d3.v4.min.js" > </script>
<script src = "http://d3js.org/topojson.v3.min.js" > </script>
<script>
            var nspiral = 12,
                dlat = 5,
                ndiv = 5,
                ndivRange = [3, 48],
                centerProjection = [-30, 0],
                rotationSpeed = 20;
        var width = 600,
            height = 320,
            SECOND = 1000;
        var spiral, timer, doRotation = true;

        var scaleProj = Math.min(width / 2, height) / Math.PI;
        var projection = d3.geoEquirectangular().translate([width / 2, height / 2]).scale(scaleProj).rotate([-centerProjection[0], -centerProjection[1]]);
        var path = d3.geoPath().projection(projection);
        var graticule = d3.geoGraticule();

        var svg = d3.select("body").append("svg")
            .attr("width", width)
            .attr("height", height)
            .on("wheel", redraw)
            .call(d3.drag()
                .subject(function() {
                    var rotate = projection.rotate();
                    return {
                        x: 2 * rotate[0],
                        y: -2 * rotate[1]
                    };
                })
                .on("start", function(d) {
                    doRotation = false;
                    clearTimeout(timer);
                })
                .on("end", function(d) {
                    restartRotation()
                })
                .on("drag", function() {
                    projection.rotate([d3.event.x / 2, -d3.event.y / 2, projection.rotate()[2]]);
                    svg.selectAll("path").attr("d", path);
                }));

        svg.selectAll(".graticule")
        .data(graticule.lines)
        .enter().append("path")
        .attr("class", "graticule")
        .attr("d", path);

        drawSpiral(ndiv);


        function restartRotation() {
            timer = setTimeout(function() {
                doRotation = true
            }, 1 * SECOND);
        }

        function redraw() {
            ndiv *= Math.exp(d3.event.deltaY * 0.004);
            ndiv = clipRange(Math.ceil(ndiv), ndivRange);
            drawSpiral(ndiv);
        }

        function clipRange(x, xRange) {
            return Math.min(Math.max(x, xRange[0]), xRange[1]);
        }

        function makeSpiral(t, d) {
            if (!d) {
                d = 0
            }
            return [normalise(360 * nspiral * t), (180 - dlat) * t - 90 + dlat * d];
        }


        function normalise(x) {
            return (x + 180) % 360 - 180;
        }

        function drawSpiral(ndiv) {
            ndiv *= nspiral;
            if (spiral) {
                spiral.remove()
            }
            spiral = d3.range(0, ndiv + 1).map(function(w) {
                return makeSpiral(w / ndiv, 1);
            }).concat(d3.range(ndiv, -1, -1).map(function(w) {
                return makeSpiral(w / ndiv);
            }));
            spiral.push(spiral[0]);

            spiral = svg.append("path")
                .datum({
                    type: "Polygon",
                    coordinates: [spiral]
                })
                .attr("class", "spiral")
                .attr("d", path);
        }

        var url = "https://unpkg.com/world-atlas@1/world/110m.json";
        d3.json(url, function(error, world) {
            svg.insert("path", ".graticule")
                .datum(topojson.feature(world, world.objects.land))
                .attr("class", "land")
                .attr("d", path);

            svg.insert("g", ".graticule")
                .datum(topojson.mesh(world, world.objects.countries, function(a, b) {
                    return a !== b;
                }))
                .attr("class", "boundary")
                .attr("d", path);

            function autoRotate() {
                if (doRotation) {
                    var o0 = projection.rotate();
                    o0[0] += 1;
                    projection.rotate(o0);
                    svg.selectAll("path")
                        .attr("d", path);
                }
            }

            setInterval(autoRotate, rotationSpeed);
        
        });

</script>
 </body>
```

------------------