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
xxxxxxxxxx
<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>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js
https://api.mapbox.com/mapbox.js/v2.2.3/mapbox.js