block by wboykinm 59c11555cf2e7463073dc018804589c4

geosnap

Full Screen

Playing around with the idea of snapping to a latlon grid, by rounding the latlon values of my mouse coordinates to 1 decimal place.

Not exactly sure how this will be useful, but it could be an interesting way to interact with a values binned to a grid on the globe.

Built with blockbuilder.org

forked from enjalot‘s block: geosnap

forked from anonymous‘s block: geosnap

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3-geo-projection/0.2.9/d3.geo.projection.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script>
  <script src="trackball.js"></script>
  <style>
    body { 
      margin:0;position:fixed;top:0;right:0;bottom:0;left:0; 
      background-color: #36637F;
    }
    svg { width: 100%; height: 100%; }
    .graticule {
      fill: none;
      stroke: #333;
      stroke-width: .5px;
      stroke-opacity: .5;
    }

    .land {
      fill: #1C3233;
    }

    .boundary {
      fill: none;
      stroke: #777;
      stroke-width: .5px;
    }
    circle {
      fill: #EEECC9;
      pointer-events: none;
    }
  </style>
</head>

<body>
  <svg></svg>
  <script>
    var width = 960;
    var height = 500;
    var svg = d3.select("svg");
    
    var scale = (width - 1) / 2 / Math.PI * 3
    
    var projection = d3.geo.orthographic()
    .scale(scale)
    .translate([width / 2, height / 2])
    .clipAngle(90)
    .precision(.1);
    
    var zoom = d3.behavior.zoom()
    .translate([width / 2, height / 2])
    .scale(scale)
    .scaleExtent([scale, 8 * scale])
    .on("zoom", zoomed)

    var path = d3.geo.path()
        .projection(projection);

    var graticule = d3.geo.graticule();
    svg.append("path")
    .datum(graticule)
    .attr("class", "graticule")
    .attr("d", path);
    
    d3.json("world-110m.json", function(err, world) {
      svg.append("path")
        .datum(topojson.feature(world, world.objects.land))
        .attr("class", "land")
        .attr("d", path);

      svg.append("path")
        .datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
        .attr("class", "boundary")
        .attr("d", path);
      
    })
    
    svg.call(zoom)
      .call(zoom.event);
    
    function zoomed() {
      projection
        .scale(zoom.scale())
      
			update(projection.rotate());
    }

    d3.behavior.trackball(svg).on("rotate", update);
		function update(rot) {
			//update the rotation in our projection
      
      projection.rotate(rot);
      
      d3.selectAll("path")
      .attr("d", path);
      
      //console.log("updating circles")
      makePoints();
      
    }

    
    svg.on("mousemove.circles", makePoints);
           
    function makePoints() {
      var xy = d3.mouse(svg.node());
      var latlon = projection.invert(xy)
      if(isNaN(latlon[0]) || isNaN(latlon[1])) {
        return;
      }
      //console.log("xy", xy, "latlon", latlon)
      
      //rounding to degree (whole numbers)
      var p = [round(latlon[0]), round(latlon[1])];
      var points = generateSquare(p[0], p[1], 20, 1);
      console.log("points", points[0])
      var rxy = projection(p);
      
      var circles = svg.selectAll("circle").data(points)
      circles.enter().append("circle")
      circles
      .attr({
        r: function(d) { return 3 },
        "fill-opacity": function(d) { return 1-d.d/10 },
        cx: function(d) { return projection([d.x, d.y])[0] },
        cy: function(d) { return projection([d.x, d.y])[1] }
      })
    }
    
    function generateSquare(cx, cy, width, step) {
      //generate points that fill in a square around a center
			var nside = Math.floor(width / step + 0.5); //rounded
      var points = [];
      for(var i = 0; i < nside; i++) {
        for(var j = 0; j < nside; j++) {
          var x = cx - width/2 + i * step;
          var y = cy - width/2 + j * step;
          points.push({
            x: x,
            y: y,
            d: Math.sqrt((cx-x)*(cx-x) + (cy-y)*(cy-y))
          })
        }
      }
      return points;
    }

    function round(deg, decimals) {
      if(!decimals) decimals = 0;
      var factor = Math.pow(10, decimals);
      return Math.floor(deg * factor + 0.5)/factor;
    }
    //hover over space in the globe, see circles at rounded locations
    // transition in size
    console.log(round(77.213, 2))
    
    //place rounded navigator.geolocation on globe using pubnub
    
    navigator.geolocation.getCurrentPosition(function(position) {
      // this doesn't seem to work in blockbuilder.org because of the
      // iframe sandboxing
      console.log("position", position);
    });

  </script>
</body>

trackball.js

// this isn't structured like a proper behavior. something
// that would probably be useful, for now I just want to encapsulate it
// all the code below comes from http://bl.ocks.org/patricksurry/5721459

d3.behavior.trackball = function(svg) {
  
svg
  .on("mousedown", mousedown)
  .on("mousemove", mousemove)
  .on("mouseup", mouseup);
  
function trackballAngles(pt) {
  // based on http://www.opengl.org/wiki/Trackball  
  // given a click at (x,y) in canvas coords on the globe (trackball),
  // calculate the spherical coordianates for the point as a rotation around
  // the vertical and horizontal axes
  
  var r = projection.scale();
  var c = projection.translate();
  var x = pt[0] - c[0], y = - (pt[1] - c[1]), ss = x*x + y*y;


  var z = r*r > 2 * ss ? Math.sqrt(r*r - ss) : r*r / 2 / Math.sqrt(ss);  

  var lambda = Math.atan2(x, z) * 180 / Math.PI; 
  var phi = Math.atan2(y, z) * 180 / Math.PI
  return [lambda, phi];
}

/*
This is the cartesian equivalent of the rotation matrix, 
which is the product of the following rotations (in numbered order):
1. longitude: λ around the y axis (which points up in the canvas)
2. latitude: -ϕ around the x axis (which points right in the canvas)
3. yaw:       γ around the z axis (which points out of the screen)

NB.  If you measure rotations in a positive direction according to the right-hand rule 
(point your right thumb in the positive direction of the rotation axis, and rotate in the
direction of your curled fingers), then the latitude rotation is negative.

R(λ, ϕ, γ) = 
[[ sin(γ)sin(λ)sin(ϕ)+cos(γ)cos(λ), −sin(γ)cos(ϕ), −sin(γ)sin(ϕ)cos(λ)+sin(λ)cos(γ)],
 [ −sin(λ)sin(ϕ)cos(γ)+sin(γ)cos(λ), cos(γ)cos(ϕ), sin(ϕ)cos(γ)cos(λ)+sin(γ)sin(λ)],
 [ −sin(λ)cos(ϕ),                    −sin(ϕ),       cos(λ)cos(ϕ)]]

If you then apply a "trackball rotation" of δλ around the y axis, and -δϕ around the 
x axis, you get this horrible composite matrix:

R2(λ, ϕ, γ, δλ, δϕ) = 
[[−sin(δλ)sin(λ)cos(ϕ)+(sin(γ)sin(λ)sin(ϕ)+cos(γ)cos(λ))cos(δλ),
        −sin(γ)cos(δλ)cos(ϕ)−sin(δλ)sin(ϕ),
                sin(δλ)cos(λ)cos(ϕ)−(sin(γ)sin(ϕ)cos(λ)−sin(λ)cos(γ))cos(δλ)],
 [−sin(δϕ)sin(λ)cos(δλ)cos(ϕ)−(sin(γ)sin(λ)sin(ϕ)+cos(γ)cos(λ))sin(δλ)sin(δϕ)−(sin(λ)sin(ϕ)cos(γ)−sin(γ)cos(λ))cos(δϕ),
        sin(δλ)sin(δϕ)sin(γ)cos(ϕ)−sin(δϕ)sin(ϕ)cos(δλ)+cos(δϕ)cos(γ)cos(ϕ),
                sin(δϕ)cos(δλ)cos(λ)cos(ϕ)+(sin(γ)sin(ϕ)cos(λ)−sin(λ)cos(γ))sin(δλ)sin(δϕ)+(sin(ϕ)cos(γ)cos(λ)+sin(γ)sin(λ))cos(δϕ)],
 [−sin(λ)cos(δλ)cos(δϕ)cos(ϕ)−(sin(γ)sin(λ)sin(ϕ)+cos(γ)cos(λ))sin(δλ)cos(δϕ)+(sin(λ)sin(ϕ)cos(γ)−sin(γ)cos(λ))sin(δϕ),
        sin(δλ)sin(γ)cos(δϕ)cos(ϕ)−sin(δϕ)cos(γ)cos(ϕ)−sin(ϕ)cos(δλ)cos(δϕ),
                cos(δλ)cos(δϕ)cos(λ)cos(ϕ)+(sin(γ)sin(ϕ)cos(λ)−sin(λ)cos(γ))sin(δλ)cos(δϕ)−(sin(ϕ)cos(γ)cos(λ)+sin(γ)sin(λ))sin(δϕ)]]
                
by equating components of the matrics 
(label them [[a00, a01, a02], [a10, a11, a12], [a20, a21, a22]])
we can find an equivalent rotation R(λ', ϕ', γ') == RC(λ, ϕ, γ, δλ, δϕ) :
 
if cos(ϕ') != 0:
 γ' = atan2(-RC01, RC11)
 ϕ' = atan2(-RC21, γ' == 0 ? RC11 / cos(γ') : - RC01 / sin(γ'))
 λ' = atan2(-RC20, RC22)
else:
 // when cos(ϕ') == 0, RC21 == - sin(ϕ') == +/- 1
 // the solution is degenerate, requiring just that
 //    γ' - λ' = atan2(RC00, RC10) if RC21 == -1 (ϕ' = π/2)
 // or γ' + λ' = atan2(RC00, RC10) if RC21 == 1 (ϕ' = -π/2)
 // so choose:
 γ' = atan2(RC10, RC00) - RC21 * λ
 ϕ' = - RC21 * π/2
 λ' = λ

*/

function composedRotation(λ, ϕ, γ, δλ, δϕ) {
    λ = Math.PI / 180 * λ;
    ϕ = Math.PI / 180 * ϕ;
    γ = Math.PI / 180 * γ;
    δλ = Math.PI / 180 * δλ;
    δϕ = Math.PI / 180 * δϕ;
    
    var sλ = Math.sin(λ), sϕ = Math.sin(ϕ), sγ = Math.sin(γ), 
        sδλ = Math.sin(δλ), sδϕ = Math.sin(δϕ),
        cλ = Math.cos(λ), cϕ = Math.cos(ϕ), cγ = Math.cos(γ), 
        cδλ = Math.cos(δλ), cδϕ = Math.cos(δϕ);

    var m00 = -sδλ * sλ * cϕ + (sγ * sλ * sϕ + cγ * cλ) * cδλ,
            m01 = -sγ * cδλ * cϕ - sδλ * sϕ,
                m02 = sδλ * cλ * cϕ - (sγ * sϕ * cλ - sλ * cγ) * cδλ,
        m10 = - sδϕ * sλ * cδλ * cϕ - (sγ * sλ * sϕ + cγ * cλ) * sδλ * sδϕ - (sλ * sϕ * cγ - sγ * cλ) * cδϕ,
            m11 = sδλ * sδϕ * sγ * cϕ - sδϕ * sϕ * cδλ + cδϕ * cγ * cϕ,
                 m12 = sδϕ * cδλ * cλ * cϕ + (sγ * sϕ * cλ - sλ * cγ) * sδλ * sδϕ + (sϕ * cγ * cλ + sγ * sλ) * cδϕ,
        m20 = - sλ * cδλ * cδϕ * cϕ - (sγ * sλ * sϕ + cγ * cλ) * sδλ * cδϕ + (sλ * sϕ * cγ - sγ * cλ) * sδϕ,
            m21 = sδλ * sγ * cδϕ * cϕ - sδϕ * cγ * cϕ - sϕ * cδλ * cδϕ,
                 m22 = cδλ * cδϕ * cλ * cϕ + (sγ * sϕ * cλ - sλ * cγ) * sδλ * cδϕ - (sϕ * cγ * cλ + sγ * sλ) * sδϕ;
                 
    if (m01 != 0 || m11 != 0) {
         γ_ = Math.atan2(-m01, m11);
         ϕ_ = Math.atan2(-m21, Math.sin(γ_) == 0 ? m11 / Math.cos(γ_) : - m01 / Math.sin(γ_));
         λ_ = Math.atan2(-m20, m22);
    } else {
         γ_ = Math.atan2(m10, m00) - m21 * λ;
         ϕ_ = - m21 * Math.PI / 2;
         λ_ = λ;       
    }
    
    return([λ_ * 180 / Math.PI, ϕ_ * 180 / Math.PI, γ_ * 180 / Math.PI]);
}
    
var m0 = null,
    o0;

var dispatch = d3.dispatch("rotate")
function mousedown() {  // remember where the mouse was pressed, in canvas coords
  m0 = trackballAngles(d3.mouse(svg[0][0]));
  o0 = projection.rotate();
  d3.event.preventDefault();
}

function mousemove() {
  if (m0) {  // if mousedown
    var m1 = trackballAngles(d3.mouse(svg[0][0]));
    // we want to find rotate the current projection so that the point at m0 rotates to m1
    // along the great circle arc between them.
    // when the current projection is at rotation(0,0), with the north pole aligned
    // to the vertical canvas axis, and the equator aligned to the horizontal canvas
    // axis, this is easy to do, since D3's longitude rotation corresponds to trackball
    // rotation around the vertical axis, and then the subsequent latitude rotation 
    // corresponds to the trackball rotation around the horizontal axis.
    // But if the current projection is already rotated, it's harder.  
    // We need to find a new rotation equivalent to the composition of both
    
    // Choose one of these three update schemes:
    
    // Best behavior
    o1 = composedRotation(o0[0], o0[1], o0[2], m1[0] - m0[0], m1[1] - m0[1])
    
    // Improved behavior over original example
    //o1 = [o0[0] + (m1[0] - m0[0]), o0[1] + (m1[1] - m0[1])];
    
    // Original example from http://mbostock.github.io/d3/talk/20111018/azimuthal.html
    // o1 = [o0[0] - (m0[0] - m1[0]) / 8, o0[1] - (m1[1] - m0[1]) / 8];

    // move to the updated rotation
    dispatch.rotate(o1);
    //projection.rotate(o1);
    
    // We can optionally update the "origin state" at each step.  This has the 
    // advantage that each 'trackball movement' is small, but the disadvantage of
    // potentially accumulating many small drifts (you often see a twist creeping in
    // if you keep rolling the globe around with the mouse button down)    
//    o0 = o1;
//    m0 = m1;

    //svg.selectAll("path").attr("d", path); 
  }
}

function mouseup() {
  if (m0) {
    mousemove();
    m0 = null;
  }
}
  
  return dispatch;
}