Last active
April 11, 2024 08:33
-
-
Save zross/6a31f4ef9e778d94c204 to your computer and use it in GitHub Desktop.
Animate path on Leaflet map using D3 (Gimme!, Proletariat)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7/leaflet.css" /> | |
<script src="http://d3js.org/d3.v3.min.js" type="text/javascript"></script> | |
<script src="http://cdn.leafletjs.com/leaflet-0.7/leaflet.js"></script> | |
<script src='https://api.tiles.mapbox.com/mapbox.js/v1.6.4/mapbox.js'></script> | |
<link href='https://api.tiles.mapbox.com/mapbox.js/v1.6.4/mapbox.css' rel='stylesheet' /> | |
<style> | |
html, | |
body { | |
height: 100%; | |
width: 100%; | |
} | |
body { | |
margin: 0; | |
} | |
#map { | |
width: 100%; | |
height: 100%; | |
} | |
svg { | |
position: relative; | |
} | |
path { | |
fill: yellow; | |
stroke-width: 2px; | |
stroke: red; | |
stroke-opacity: 1; | |
} | |
.travelMarker { | |
fill: yellow; | |
opacity: 0.75; | |
} | |
.waypoints { | |
fill: black; | |
opacity: 0; | |
} | |
} | |
.drinks { | |
stroke: black; | |
fill: red; | |
} | |
.lineConnect { | |
fill: none; | |
stroke: black; | |
opacity: 1; | |
} | |
.locnames { | |
fill: black; | |
text-shadow: 1px 1px 1px #FFF, 3px 3px 5px #000; | |
font-weight: bold; | |
font-size: 13px; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="demo"></div> | |
<div id="map"></div> | |
<script type="text/javascript"> | |
var mapboxTiles = L.tileLayer('https://{s}.tiles.mapbox.com/v3/examples.map-zr0njcqy/{z}/{x}/{y}.png', { | |
attribution: '<a href="http://www.mapbox.com/about/maps/" target="_blank">Terms & Feedback</a>' | |
}); | |
var map = L.map('map') | |
.addLayer(mapboxTiles) | |
.setView([40.72332345541449, -73.99], 14); | |
// we will be appending the SVG to the Leaflet map pane | |
// g (group) element will be inside the svg | |
var svg = d3.select(map.getPanes().overlayPane).append("svg"); | |
// if you don't include the leaflet-zoom-hide when a | |
// user zooms in or out you will still see the phantom | |
// original SVG | |
var g = svg.append("g").attr("class", "leaflet-zoom-hide"); | |
//read in the GeoJSON. This function is asynchronous so | |
// anything that needs the json file should be within | |
d3.json("points.geojson", function(collection) { | |
// this is not needed right now, but for future we may need | |
// to implement some filtering. This uses the d3 filter function | |
// featuresdata is an array of point objects | |
var featuresdata = collection.features.filter(function(d) { | |
return d.properties.id == "route1" | |
}) | |
//stream transform. transforms geometry before passing it to | |
// listener. Can be used in conjunction with d3.geo.path | |
// to implement the transform. | |
var transform = d3.geo.transform({ | |
point: projectPoint | |
}); | |
//d3.geo.path translates GeoJSON to SVG path codes. | |
//essentially a path generator. In this case it's | |
// a path generator referencing our custom "projection" | |
// which is the Leaflet method latLngToLayerPoint inside | |
// our function called projectPoint | |
var d3path = d3.geo.path().projection(transform); | |
// Here we're creating a FUNCTION to generate a line | |
// from input points. Since input points will be in | |
// Lat/Long they need to be converted to map units | |
// with applyLatLngToLayer | |
var toLine = d3.svg.line() | |
.interpolate("linear") | |
.x(function(d) { | |
return applyLatLngToLayer(d).x | |
}) | |
.y(function(d) { | |
return applyLatLngToLayer(d).y | |
}); | |
// From now on we are essentially appending our features to the | |
// group element. We're adding a class with the line name | |
// and we're making them invisible | |
// these are the points that make up the path | |
// they are unnecessary so I've make them | |
// transparent for now | |
var ptFeatures = g.selectAll("circle") | |
.data(featuresdata) | |
.enter() | |
.append("circle") | |
.attr("r", 3) | |
.attr("class", "waypoints"); | |
// Here we will make the points into a single | |
// line/path. Note that we surround the featuresdata | |
// with [] to tell d3 to treat all the points as a | |
// single line. For now these are basically points | |
// but below we set the "d" attribute using the | |
// line creator function from above. | |
var linePath = g.selectAll(".lineConnect") | |
.data([featuresdata]) | |
.enter() | |
.append("path") | |
.attr("class", "lineConnect"); | |
// This will be our traveling circle it will | |
// travel along our path | |
var marker = g.append("circle") | |
.attr("r", 10) | |
.attr("id", "marker") | |
.attr("class", "travelMarker"); | |
// For simplicity I hard-coded this! I'm taking | |
// the first and the last object (the origin) | |
// and destination and adding them separately to | |
// better style them. There is probably a better | |
// way to do this! | |
var originANDdestination = [featuresdata[0], featuresdata[17]] | |
var begend = g.selectAll(".drinks") | |
.data(originANDdestination) | |
.enter() | |
.append("circle", ".drinks") | |
.attr("r", 5) | |
.style("fill", "red") | |
.style("opacity", "1"); | |
// I want names for my coffee and beer | |
var text = g.selectAll("text") | |
.data(originANDdestination) | |
.enter() | |
.append("text") | |
.text(function(d) { | |
return d.properties.name | |
}) | |
.attr("class", "locnames") | |
.attr("y", function(d) { | |
return -10 | |
}) | |
// when the user zooms in or out you need to reset | |
// the view | |
map.on("viewreset", reset); | |
// this puts stuff on the map! | |
reset(); | |
transition(); | |
// Reposition the SVG to cover the features. | |
function reset() { | |
var bounds = d3path.bounds(collection), | |
topLeft = bounds[0], | |
bottomRight = bounds[1]; | |
// here you're setting some styles, width, heigh etc | |
// to the SVG. Note that we're adding a little height and | |
// width because otherwise the bounding box would perfectly | |
// cover our features BUT... since you might be using a big | |
// circle to represent a 1 dimensional point, the circle | |
// might get cut off. | |
text.attr("transform", | |
function(d) { | |
return "translate(" + | |
applyLatLngToLayer(d).x + "," + | |
applyLatLngToLayer(d).y + ")"; | |
}); | |
// for the points we need to convert from latlong | |
// to map units | |
begend.attr("transform", | |
function(d) { | |
return "translate(" + | |
applyLatLngToLayer(d).x + "," + | |
applyLatLngToLayer(d).y + ")"; | |
}); | |
ptFeatures.attr("transform", | |
function(d) { | |
return "translate(" + | |
applyLatLngToLayer(d).x + "," + | |
applyLatLngToLayer(d).y + ")"; | |
}); | |
// again, not best practice, but I'm harding coding | |
// the starting point | |
marker.attr("transform", | |
function() { | |
var y = featuresdata[0].geometry.coordinates[1] | |
var x = featuresdata[0].geometry.coordinates[0] | |
return "translate(" + | |
map.latLngToLayerPoint(new L.LatLng(y, x)).x + "," + | |
map.latLngToLayerPoint(new L.LatLng(y, x)).y + ")"; | |
}); | |
// Setting the size and location of the overall SVG container | |
svg.attr("width", bottomRight[0] - topLeft[0] + 120) | |
.attr("height", bottomRight[1] - topLeft[1] + 120) | |
.style("left", topLeft[0] - 50 + "px") | |
.style("top", topLeft[1] - 50 + "px"); | |
// linePath.attr("d", d3path); | |
linePath.attr("d", toLine) | |
// ptPath.attr("d", d3path); | |
g.attr("transform", "translate(" + (-topLeft[0] + 50) + "," + (-topLeft[1] + 50) + ")"); | |
} // end reset | |
// the transition function could have been done above using | |
// chaining but it's cleaner to have a separate function. | |
// the transition. Dash array expects "500, 30" where | |
// 500 is the length of the "dash" 30 is the length of the | |
// gap. So if you had a line that is 500 long and you used | |
// "500, 0" you would have a solid line. If you had "500,500" | |
// you would have a 500px line followed by a 500px gap. This | |
// can be manipulated by starting with a complete gap "0,500" | |
// then a small line "1,500" then bigger line "2,500" and so | |
// on. The values themselves ("0,500", "1,500" etc) are being | |
// fed to the attrTween operator | |
function transition() { | |
linePath.transition() | |
.duration(7500) | |
.attrTween("stroke-dasharray", tweenDash) | |
.each("end", function() { | |
d3.select(this).call(transition);// infinite loop | |
}); | |
} //end transition | |
// this function feeds the attrTween operator above with the | |
// stroke and dash lengths | |
function tweenDash() { | |
return function(t) { | |
//total length of path (single value) | |
var l = linePath.node().getTotalLength(); | |
// this is creating a function called interpolate which takes | |
// as input a single value 0-1. The function will interpolate | |
// between the numbers embedded in a string. An example might | |
// be interpolatString("0,500", "500,500") in which case | |
// the first number would interpolate through 0-500 and the | |
// second number through 500-500 (always 500). So, then | |
// if you used interpolate(0.5) you would get "250, 500" | |
// when input into the attrTween above this means give me | |
// a line of length 250 followed by a gap of 500. Since the | |
// total line length, though is only 500 to begin with this | |
// essentially says give me a line of 250px followed by a gap | |
// of 250px. | |
interpolate = d3.interpolateString("0," + l, l + "," + l); | |
//t is fraction of time 0-1 since transition began | |
var marker = d3.select("#marker"); | |
// p is the point on the line (coordinates) at a given length | |
// along the line. In this case if l=50 and we're midway through | |
// the time then this would 25. | |
var p = linePath.node().getPointAtLength(t * l); | |
//Move the marker to that point | |
marker.attr("transform", "translate(" + p.x + "," + p.y + ")"); //move marker | |
console.log(interpolate(t)) | |
return interpolate(t); | |
} | |
} //end tweenDash | |
// Use Leaflet to implement a D3 geometric transformation. | |
// the latLngToLayerPoint is a Leaflet conversion method: | |
//Returns the map layer point that corresponds to the given geographical | |
// coordinates (useful for placing overlays on the map). | |
function projectPoint(x, y) { | |
var point = map.latLngToLayerPoint(new L.LatLng(y, x)); | |
this.stream.point(point.x, point.y); | |
} //end projectPoint | |
}); | |
// similar to projectPoint this function converts lat/long to | |
// svg coordinates except that it accepts a point from our | |
// GeoJSON | |
function applyLatLngToLayer(d) { | |
var y = d.geometry.coordinates[1] | |
var x = d.geometry.coordinates[0] | |
return map.latLngToLayerPoint(new L.LatLng(y, x)) | |
} | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"type": "FeatureCollection", | |
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, | |
"features": [ | |
{ "type": "Feature", "properties": { "latitude": 40.722390, "longitude": -73.995170, "time": 1, "id": "route1", "name":"Gimme" }, "geometry": { "type": "Point", "coordinates": [ -73.99517, 40.72239 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.721580, "longitude": -73.995480, "time": 2, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.99548, 40.72158 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.721280, "longitude": -73.994720, "time": 3, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.99472, 40.72128 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.720890, "longitude": -73.993760, "time": 4, "id": "route1" , "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.99376, 40.72089 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.721580, "longitude": -73.993480, "time": 5, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.99348, 40.72158 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.722310, "longitude": -73.993220, "time": 6, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.99322, 40.72231 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.722470, "longitude": -73.993160, "time": 7, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.99316, 40.72247 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.723580, "longitude": -73.992740, "time": 8, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.99274, 40.72358 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.725190, "longitude": -73.992150, "time": 9, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.99215, 40.72519 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.725590, "longitude": -73.992020, "time": 10, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.99202, 40.72559 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.725390, "longitude": -73.991510, "time": 11, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.99151, 40.72539 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.724180, "longitude": -73.988650, "time": 12, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.98865, 40.72418 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.723900, "longitude": -73.987980, "time": 13, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.98798, 40.7239 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.724550, "longitude": -73.987510, "time": 14, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.98751, 40.72455 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.726370, "longitude": -73.986180, "time": 15, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.98618, 40.72637 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.726960, "longitude": -73.985750, "time": 16, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.98575, 40.72696 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.727570, "longitude": -73.985310, "time": 17, "id": "route1", "name":"Along route" }, "geometry": { "type": "Point", "coordinates": [ -73.98531, 40.72757 ] } }, | |
{ "type": "Feature", "properties": { "latitude": 40.727250, "longitude": -73.984550, "time": 18, "id": "route1", "name":"Proletariat" }, "geometry": { "type": "Point", "coordinates": [ -73.98455, 40.72725 ] } } | |
] | |
} |
Did you find a solution to your problem @mz14611?
How about using MultiLineString?
I'm having some issues with the initial set up. Are there specific packages you are running.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi, I am new in d3. I wonder how to animate multiple path and is there any limitation when read geojson data, like how large the file can be?