block by enjalot 7a8dd01fe61fba5fea45

dots on a map: circle

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

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

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) {
      if(!ll) return;
      // 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);
      
      var circleControl = new circleSelector(svg)
        .projection(project)
        .inverseProjection(function(a) {
          return map.containerPointToLatLng(L.point(a));
        })
        .activate(true);
      
      circleControl.on("update", function() {
        g.selectAll("circle.dot").style({
          fill: function(d) {
            var thisDist = circleControl.distance(d);
            var circleDist = circleControl.distance()
            if(thisDist < circleDist) {
              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},
        })
        
        // Render our selector, and update it whenever the map updates;
        circleControl.update(g)
      }
      // re-render our visualization whenever the view changes
      map.on("viewreset", function() {
        render()
      })
      // render our initial visualization
      render()
    })
    
    
    
    /* 
    ===============================================================
    Circle Selection component
    ===============================================================
    */
    function circleSelector(svg) {
      var that = this;
      var circleCenter, circleOuter; //control points
      var circleSelected = false; //have we completed the circle?
      var dragging = false; //track whether we are dragging
      var active = false; // user can turn on/off this behavior
      var container; // the container we render our points in

      // this will likely be overriden by leaflet projection
      var project = d3.geo.mercator();
      var unproject = d3.geo.mercator().invert;

      //we expose events on our component
      var dispatch = d3.dispatch("update");
      
      // The user provides an svg element to listen on events
      svg.on("mouseup.circle", function() {
        if(!active) return;
        if(dragging && circleSelected) return;
        
        var p = d3.mouse(this);
        var ll = unproject([p[0],p[1]])
        
        if(circleCenter) {
          // if we already have the circle's center and the circle
          // is finished selecting, another click means destroy the circle
          if(circleSelected) {
            // start over
            circleCenter = null;
            circleOuter = null;
            circleSelected = false;
            g.selectAll("circle.lasso").remove();
            g.selectAll("circle.control").remove();
            g.selectAll("circle.dot").style("fill", "#0082a3")
            g.selectAll("line.lasso").remove();
          } else {
            // Set the outer point
            circleOuter = ll;
            circleSelected = true;
          }
        } else {
          // We set the center to the initial click
          circleCenter = ll;
          circleOuter = ll;
        }
        // we let the user know 
        update()
      })
      svg.on("mousemove.circle", function() {
        if(circleSelected) return;
        // we draw a guideline for where the next point would go.
        var p = d3.mouse(this);
        var ll = unproject([p[0],p[1]])
        circleOuter = ll;
        update();
      })
      
      var drag = d3.behavior.drag()
        .on("drag", function(d,i) {
          if(circleSelected) {
            dragging = true;
            var p = d3.mouse(svg.node());
            var ll = unproject([p[0],p[1]])
            if(i) {
              circleOuter = ll;
            } else {
              var dlat = circleCenter.lat - ll.lat;
              var dlng = circleCenter.lng - ll.lng;
              circleCenter = ll;
              circleOuter.lat -= dlat;
              circleOuter.lng -= dlng;
            }
            update();
          } else {
            return false;
          }
        })
        .on("dragend", function(d) {
          // kind of a dirty hack...
          setTimeout(function() {
            dragging = false;
          },100)
        })
      
      function update(g) {
        if(g) container = g;
        if(!circleCenter || !circleOuter) return;
        var dist = distance(circleCenter, circleOuter)
        var circleLasso = container.selectAll("circle.lasso").data([dist])
        circleLasso.enter().append("circle").classed("lasso", true)
        circleLasso
        .attr({
          cx: project(circleCenter).x,
          cy: project(circleCenter).y,
          r: dist
        })
        .style({
          stroke: "#010",
          fill: "#010",
          "fill-opacity": 0.1
        })
        
        var line = container.selectAll("line.lasso").data([circleOuter])
        line.enter().append("line").classed("lasso", true)
        
        if(!circleSelected && circleCenter || dragging) {
          line.attr({
            x1: project(circleCenter).x,
            y1: project(circleCenter).y,
            x2: project(circleOuter).x,
            y2: project(circleOuter).y
          })
          .style({
            stroke: "#111",
            "stroke-dasharray": "5 5"
          })
        } else {
          line.remove();
        }
        
        var controls = container.selectAll("circle.control")
        .data([circleCenter, circleOuter])
        controls.enter().append("circle").classed("control", true)
        controls.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
        })
        .call(drag)
        .on("click.control", function(d) {
          if(circleSelected)
            d3.event.stopPropagation();
        })
        
        dispatch.update();
      }
      this.update = update;
      
      this.projection = function(val) {
        if(!val) return project;
        project = val;
        return this;
      }
      
      this.inverseProjection = function(val) {
        if(!val) return unproject;
        unproject = val;
        return this;
      }
      this.activate = function(val) {
        active = val;
        return this;
      }
      
      this.distance = function(ll) {
        if(!ll) ll = circleOuter;
        return distance(circleCenter, ll)
      }
      
      function distance(ll0, ll1) {
        var p0 = project(ll0)
        var p1 = project(ll1)
        var dist = Math.sqrt((p1.x - p0.x)*(p1.x - p0.x) + (p1.y - p0.y)*(p1.y-p0.y))
        return dist;
      }
      
      d3.rebind(this, dispatch, "on")
      return this;
    }

  </script>
</body>