block by enjalot b95748220bd4752fb33a

dots on a map: lasso

Full Screen

Prototyping a lasso tool for selecting geographic points with an arbitrary polygon.

Using this point-in-polygon code to calculate which points are selected within the polygon. All calculations are done in the projected (pixel) space.

Built with blockbuilder.org

forked from enjalot‘s block: dots on a map: setup

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://api.mapbox.com/mapbox.js/v2.2.3/mapbox.js'></script>
  <link href='https://api.mapbox.com/mapbox.js/v2.2.3/mapbox.css' rel='stylesheet' />
  <style>
    body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
    #map { 
      position:absolute; 
      width: 100%;
      height: 100%;
    }
    svg { 
    }
  </style>
</head>

<body>
  <div id="map"></div>
  <script>
   
    L.mapbox.accessToken = 'pk.eyJ1IjoiZW5qYWxvdCIsImEiOiJjaWhtdmxhNTIwb25zdHBsejk0NGdhODJhIn0.2-F2hS_oTZenAWc0BMf_uw'
    
    //Setup our Leaflet map using Mapbox.js
    var map = L.mapbox.map('map', 'mapbox.pencil', {
      maxZoom: 18, minZoom: 14,
      zoomControl: false
    })
    .setView([51.5119112,-0.10000], 15);
    
    // Disable drag and zoom handlers.
    map.dragging.disable();
    map.touchZoom.disable();
    map.doubleClickZoom.disable();
    map.scrollWheelZoom.disable();

// Disable tap handler, if present.
if (map.tap) map.tap.disable();
    
    // Setup our svg layer that we can manipulate with d3
    var svg = d3.select(map.getPanes().overlayPane)
      .append("svg");
    var g = svg.append("g").attr("class", "leaflet-zoom-hide");
    
    function project(ll) {
      // our data came from csv, make it Leaflet friendly
      var a = [+ll.lat, +ll.lng]; 
      // convert it to pixel coordinates
      var point = map.latLngToLayerPoint(L.latLng(ll))
      return point;
    }
  
    d3.csv("dots.csv", function(err, data) {
      var dots = g.selectAll("circle.dot")
        .data(data)
      
      dots.enter().append("circle").classed("dot", true)
      .attr("r", 1)
      .style({
        fill: "#0082a3",
        "fill-opacity": 0.6,
        stroke: "#004d60",
        "stroke-width": 1
      })
      .transition().duration(1000)
      .attr("r", 6);
      
      // we store all the state for our lasso here
      // in a bigger application we would encapsulate this
      // into a component.
      var lassoPoints = [];
      var lassoClosed = false;
      var dragging = false;
      
      svg.on("click.lasso", function() {
        if(dragging) return;
        var p = d3.mouse(this);
        var ll = map.containerPointToLatLng(L.point([p[0],p[1]]))
        
        // check if we clicked within a radius of first point
        // this will close our loop
        if(lassoPoints.length) {
        	var fp = project(lassoPoints[0])
          var dist2 = (fp.x - p[0])*(fp.x - p[0]) + (fp.y - p[1])*(fp.y-p[1])
          if(dist2 < 100) {
            lassoClosed = true;
            renderLasso();
            g.selectAll("line.lasso").remove();
            return;
          }
        }
        if(lassoClosed) {
          /*
          lassoClosed = false;
          g.selectAll(".lasso").remove();
          lassoPoints = [];
          return render();
          */
          return;
        };
        
        lassoPoints.push(ll);
        render();
      })
      svg.on("mousemove", function() {
        // we draw a guideline for where the next point would go.
        var lastPoint = lassoPoints[lassoPoints.length-1];
        var p = d3.mouse(this);
        var ll = map.containerPointToLatLng(L.point([p[0],p[1]]));
        
        var line = g.selectAll("line.lasso").data([lastPoint])
        line.enter().append("line").classed("lasso", true)
        
        if(lassoPoints.length && !lassoClosed) {
          line.attr({
            x1: project(lastPoint).x,
            y1: project(lastPoint).y,
            x2: project(ll).x,
            y2: project(ll).y
          })
          .style({
            stroke: "#111",
            "stroke-dasharray": "5 5"
          })
        } else {
          line.remove();
        }
      })
      
      var path = d3.svg.line()
			  .x(function(d) { return project(d).x})
        .y(function(d) { return project(d).y})
        //.interpolate("cardinal")
      
      function renderLasso() {
        // render our lasso
        
        var lassoPath = g.selectAll("path.lasso").data([lassoPoints])
        lassoPath.enter().append("path").classed("lasso", true)
        .on("click", function() {
          if(lassoClosed) {
            lassoClosed = false;
            g.selectAll(".lasso").remove();
            lassoPoints = [];
            d3.event.stopPropagation();
            return render();
          };
        })
        
        lassoPath.attr("d", function(d) { 
          var str = path(d)
          if(lassoClosed) str += "Z"
          return str;
        })
        .style({
          stroke: "#010",
          fill: "#010",
          "fill-opacity": 0.1
        })
        
        var drag = d3.behavior.drag()
        .on("drag", function(d) {
          if(!lassoClosed) return;
          dragging = true;
          var p = d3.mouse(svg.node())
          var ll = map.containerPointToLatLng(L.point([p[0],p[1]]));
          d.lat = ll.lat;
          d.lng = ll.lng;
          renderLasso();
          
        }).on("dragend", function() {
          setTimeout(function() {
            dragging = false;
          }, 100)
        })
        var lasso = g.selectAll("circle.lasso")
        .data(lassoPoints)
        lasso.enter().append("circle").classed("lasso", true)
        .call(drag);
        
        lasso.attr({
          cx: function(d) { return project(d).x},
          cy: function(d) { return project(d).y},
          r: 8,
          stroke: "#010",
          fill: "#b7feb7",
          "fill-opacity":0.9
        })
        
        var projected = lassoPoints.map(function(d){
          return project(d)
        })
        
        console.log(projected)
        g.selectAll("circle.dot").style({
          fill: function(d) {
            var isInside = inside(project(d), projected);
            //console.log(project(d), isInside)
            if(isInside) {
              return "#ff8eec";
            } else {
              return "#0082a3"
            }
          }
        })
      }

      
      function render() {
        // We need to reposition our SVG and our containing group when the map
        // repositions via zoom or pan
        // https://github.com/zetter/voronoi-maps/blob/master/lib/voronoi_map.js
        var bounds = map.getBounds();
        var topLeft = map.latLngToLayerPoint(bounds.getNorthWest())
        var bottomRight = map.latLngToLayerPoint(bounds.getSouthEast())
        svg.style("width", map.getSize().x + "px")
          .style("height", map.getSize().y + "px")
          .style("left", topLeft.x + "px")
          .style("top", topLeft.y + "px");
        g.attr("transform", "translate(" + -topLeft.x + "," + -topLeft.y + ")");

        // We reproject our data with the updated projection from leaflet
        g.selectAll("circle.dot")
        .attr({
          cx: function(d) { return project(d).x},
          cy: function(d) { return project(d).y},
        })
        
        renderLasso();
      }
      


      // re-render our visualization whenever the view changes
      map.on("viewreset", function() {
        render()
      })

      // render our initial visualization
      render()
    })
    
    
    function inside(point, vs) {
    // ray-casting algorithm based on
    // //www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
    
    var x = point.x, y = point.y;
    
    var inside = false;
    for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
        var xi = vs[i].x, yi = vs[i].y;
        var xj = vs[j].x, yj = vs[j].y;
        
        var intersect = ((yi > y) != (yj > y))
            && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
        if (intersect) inside = !inside;
    }
    
    return inside;
};
    
  </script>
</body>