|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="utf-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|
<title>Anyone wanna waste some time?</title> |
|
|
|
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7/leaflet.css" /> |
|
<link rel="stylesheet" href="https://api.tiles.mapbox.com/mapbox.js/v1.6.4/mapbox.css" /> |
|
|
|
<style> |
|
html, |
|
body { |
|
height: 100%; |
|
width: 100%; |
|
} |
|
body { |
|
margin: 0; |
|
} |
|
#map { |
|
position: absolute; |
|
top : 0; |
|
bottom : 0; |
|
width : 100%; |
|
} |
|
svg { |
|
xxxoutline: 1px solid red; |
|
} |
|
path { |
|
fill : none; |
|
stroke-width : 4px; |
|
stroke : red; |
|
stroke-opacity: 0.5; |
|
transition: stroke-dashoffset 2s linear; |
|
} |
|
|
|
path.computed { |
|
stroke-dashoffset: 0; |
|
} |
|
|
|
.marker { |
|
position: relative; |
|
z-index:3000; |
|
r : 6; |
|
fill : red; |
|
stroke : none; |
|
fill-opacity: 0.5; |
|
} |
|
|
|
.marker.current { |
|
r : 8; |
|
fill-opacity: 1; |
|
} |
|
|
|
#menu { |
|
padding: 10px; |
|
} |
|
|
|
.debug .ui-coords { |
|
background : #fff; |
|
position : absolute; |
|
top : 10px; |
|
right : 10px; |
|
min-width : 300px; |
|
padding : 10px; |
|
z-index : 100; |
|
border-radius: 3px; |
|
} |
|
</style> |
|
|
|
</head> |
|
<body> |
|
<div id="output" class="ui-coords"> |
|
<h6>Click: <code id="ui-click"></code></h6> |
|
<h6>Mousemove: <code id="ui-mousemove"></code></h6> |
|
</div> |
|
<div id="map"></div> |
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></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> |
|
<script src="http://maps.google.com/maps/api/js?sensor=true"></script> |
|
<script> |
|
(function(window, undefined) { |
|
var width = 960; |
|
var height = 500; |
|
|
|
// Using keys for lat & lng rather than simple array because |
|
// we're mixing Leaflet and Google with d3, whose ordering differs. |
|
// Being explicit saves headaches. |
|
var regions = [ |
|
{ name : "Montreal", coords: {lat: 45.515940, lng: -73.583661}, zoom : 12 }, |
|
{ name : "Paris", coords: {lat: 48.860282, lng: 2.340350}, zoom : 13 }, |
|
{ name : "Amsterdam", coords: {lat: 52.371656, lng: 4.892070}, zoom : 14 }, |
|
{ name : "Chicago", coords: {lat: 41.861528, lng: -87.717861}, zoom : 11 }, |
|
{ name : "Barcelona", coords: {lat: 41.406897, lng: 2.171516}, zoom : 12 }, |
|
{ name : "Singapore", coords: {lat: 1.338881, lng: 103.850066}, zoom : 12 } |
|
]; |
|
|
|
var map, menu, svg, g, pathGenerator, directionsService, dispatch, debug; |
|
|
|
// I've no plans to support other query params so ... simplify all the things! |
|
debug = (location.search && location.search.substr(1) == "debug"); |
|
|
|
map = initLeaflet(map); |
|
|
|
// point transform and path generator |
|
var transform = d3.geo.transform({ |
|
point: streamPoint |
|
}); |
|
|
|
pathGenerator = d3.geo.path().projection(transform); |
|
|
|
// event handling |
|
dispatch = d3.dispatch("clicked", "directions", "zoom"); |
|
dispatch.on("clicked", destinationRequest); |
|
dispatch.on("directions", handleDirections); |
|
//dispatch.on("zoom", update); |
|
|
|
directionsService = new google.maps.DirectionsService(); |
|
|
|
run(map); |
|
|
|
|
|
|
|
function run(map) { |
|
// tear down previous svg if any |
|
d3.select("svg").remove(); |
|
|
|
var containerNode = d3.select(".leaflet-container").node(); |
|
var dims = getElementDimensions(containerNode); |
|
|
|
// set Leaflet view based on selected coords |
|
var initialCoords = setMapRegion(map); |
|
|
|
svg = d3.select(map.getPanes().overlayPane) |
|
.append("svg") |
|
.attr("width", dims.width) |
|
.attr("height", dims.height); |
|
|
|
var g = svg.append("g") |
|
.attr("class", "leaflet-zoom-hide"); |
|
|
|
data = initData(initialCoords); |
|
|
|
map.on("viewreset dragend resize", resetOverlay); |
|
map.on("click", handleClick); |
|
|
|
update(data, g); |
|
} |
|
|
|
|
|
function resetOverlay() { |
|
if ( data.paths.geometries.length ) { |
|
var g = d3.select("svg g"); |
|
var bounds = pathGenerator.bounds(data.paths); |
|
var topLeft = bounds[0]; |
|
var bottomRight = bounds[1]; |
|
|
|
svg |
|
.attr("width", bottomRight[0] - topLeft[0]) |
|
.attr("height", bottomRight[1] - topLeft[1]) |
|
.style("left", topLeft[0] + "px") |
|
.style("top", topLeft[1] + "px"); |
|
|
|
|
|
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"); |
|
|
|
g.attr("transform", "translate(" + (-topLeft[0] + 50) + "," + (-topLeft[1] + 50) + ")"); |
|
} |
|
update(data, g); |
|
} |
|
|
|
|
|
|
|
|
|
// click event handler fires "clicked", passing destination coords |
|
// |
|
function handleClick(e) { |
|
var dest = e.latlng; |
|
dispatch.clicked(dest); |
|
} |
|
|
|
// a destination request event triggers ... guess what? |
|
// |
|
function destinationRequest(dest) { |
|
// current marker is the last one placed on map |
|
var origin = currentMarkerCoords(data); |
|
// async: dispatch will prompt to continue when google responds |
|
getDirections(origin, dest); |
|
} |
|
|
|
// handle the response from google directions api |
|
// |
|
function handleDirections(leg) { |
|
data = extractPath(leg, data); |
|
resetOverlay(); |
|
//update(data); |
|
} |
|
|
|
|
|
// draw markers & paths |
|
// |
|
function update(data, g) { |
|
g = g || d3.select("svg g"); |
|
|
|
var paths = g.selectAll("path") |
|
.data(data.paths.geometries); |
|
|
|
paths.enter() |
|
.append("path") |
|
.call(transition); |
|
|
|
paths.attr("d", pathGenerator); |
|
|
|
var markers = g.selectAll(".marker") |
|
.classed("current", false) |
|
.data(data.markers.geometries); |
|
|
|
markers.enter() |
|
.append("circle") |
|
.attr("class", "marker"); |
|
|
|
markers |
|
.attr("cx", function(d) { return projectPoint( d.coordinates[0], d.coordinates[1] ).x; }) |
|
.attr("cy", function(d) { return projectPoint( d.coordinates[0], d.coordinates[1] ).y; }); |
|
|
|
g.select(".marker:last-of-type") |
|
.classed("current", true); |
|
} |
|
|
|
|
|
|
|
// Have CSS handle line drawing transition trickery |
|
// see: http://bl.ocks.org/brianally/b477ac1e7b4cabc8eb0b |
|
// |
|
// UPDATE: unused |
|
// |
|
function animateStroke(path) { |
|
var node = path.node(); |
|
if( node ) { |
|
var l = node.getTotalLength(); |
|
path |
|
.attr("stroke-dasharray", l + " " + l) |
|
.attr("stroke-dashoffset", l); |
|
|
|
getComputed(node).getPropertyValue("stroke-dashoffset"); |
|
path.classed("computed", true); |
|
} |
|
} |
|
|
|
|
|
function transition(path) { |
|
path.transition() |
|
.duration(2000) |
|
.attrTween("stroke-dasharray", tweenDash); |
|
} |
|
|
|
function tweenDash() { |
|
var l = this.getTotalLength(), |
|
i = d3.interpolateString("0," + l, l + "," + l); |
|
return function(t) { return i(t); }; |
|
} |
|
|
|
|
|
|
|
// make request to google directions service |
|
// |
|
function getDirections(origin, dest) { |
|
var req = { |
|
origin : new google.maps.LatLng(origin.lat, origin.lng), |
|
destination: new google.maps.LatLng(dest.lat, dest.lng), |
|
travelMode : google.maps.DirectionsTravelMode.DRIVING, |
|
unitSystem : google.maps.UnitSystem.METRIC |
|
}; |
|
|
|
directionsService.route(req, function(result, status) { |
|
if (status == google.maps.DirectionsStatus.OK) { |
|
dispatch.directions(result.routes[0].legs[0]); |
|
} else { |
|
console.error("directions request failed: %s", status); |
|
return null; |
|
} |
|
}); |
|
} |
|
|
|
// unpack google's response and add to data |
|
// |
|
function extractPath(leg, data) { |
|
var ls = { |
|
"type" : "LineString", |
|
"coordinates": [] |
|
}; |
|
|
|
var pnt = { |
|
"type" : "Point", |
|
"coordinates": [] |
|
}; |
|
|
|
leg.steps.forEach(function(step) { |
|
step.path.forEach(function(position) { |
|
ls.coordinates.push( [position.L, position.H] ); |
|
}); |
|
}); |
|
|
|
// We want to use the last point for the new marker, not the clicked |
|
// coords, because google may not send back precisely the same point. |
|
pnt.coordinates = ls.coordinates[ ls.coordinates.length - 1 ]; |
|
|
|
data.paths.geometries.push(ls); |
|
data.markers.geometries.push(pnt); |
|
|
|
if (debug) { |
|
console.log("lineString: %O", ls); |
|
console.log("point: %O", pnt); |
|
} |
|
|
|
return data; |
|
} |
|
|
|
|
|
// Get the coordinates of the most recently added marker |
|
// |
|
function currentMarkerCoords(data) { |
|
var c = data.markers.geometries[ data.markers.geometries.length - 1 ].coordinates; |
|
return { lng: c[0], lat: c[1] }; |
|
} |
|
|
|
|
|
// Set the map view according to the currently selected city and add a first |
|
// marker centred on the region. |
|
// |
|
function setMapRegion(map) { |
|
var selected = getSelectedRegion(); |
|
map.setView([selected.coords.lat, selected.coords.lng], selected.zoom); |
|
return selected.coords; |
|
} |
|
|
|
|
|
|
|
// get the currently selected region from dropdown menu |
|
// |
|
function getSelectedRegion() { |
|
var city = menu.property("value"); |
|
return regions.filter(function(d) { |
|
return d.name == city; |
|
})[0]; |
|
} |
|
|
|
|
|
// Projects a lng/lat position onto the canvas |
|
// |
|
function projectPoint(x, y) { |
|
//return map.latLngToLayerPoint(new L.LatLng(y, x)); |
|
|
|
// http://stackoverflow.com/questions/19660153/d3-leaflet-d3-geo-path-resampling |
|
return map.project(new L.LatLng(y, x))._subtract(map.getPixelOrigin()); |
|
} |
|
|
|
|
|
// Stream to add points to a path |
|
// |
|
function streamPoint(lng, lat) { |
|
var point = projectPoint(lng, lat); |
|
this.stream.point(point.x, point.y); |
|
} |
|
|
|
|
|
// creates the map object and city select menu |
|
// |
|
function initLeaflet(map) { |
|
var tileProvider = { |
|
url : "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", |
|
options: { |
|
attribution: '© <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Directions data: Google' |
|
} |
|
}; |
|
|
|
// set up Leaflet |
|
var mapboxTiles = L.tileLayer(tileProvider.url, tileProvider.options); |
|
map = L.map('map') |
|
.addLayer(mapboxTiles); |
|
|
|
// menu to change cities |
|
L.Control.CitySelectMenu = L.Control.extend({ |
|
options: { |
|
position: "topleft" |
|
}, |
|
onAdd: function (map) { |
|
var container = L.DomUtil.create("div", "menu"); |
|
var stop = L.DomEvent.stopPropagation; |
|
|
|
container.innerHTML = "<select></select>"; |
|
|
|
L.DomEvent |
|
.on(container, 'click', stop) |
|
.on(container, 'mousedown', stop) |
|
|
|
return container; |
|
} |
|
}); |
|
|
|
new L.Control.CitySelectMenu().addTo(map); |
|
|
|
menu = d3.select(".menu select"); |
|
menu.selectAll("option") |
|
.data(regions).enter() |
|
.append("option") |
|
.text(function(d) { return d.name; }); |
|
|
|
// allow to pass map to run() |
|
var mapRun = (function(map) { return function() { run(map); }; })(map); |
|
menu.on("change", mapRun); |
|
|
|
// display mouse coords in debug mode |
|
if (debug) { |
|
d3.select("body").classed("debug", true); |
|
|
|
map.on("mousemove click", function(e) { |
|
d3.select("#ui-" + e.type) |
|
.html( e.containerPoint.toString() + ", " + e.latlng.toString() ); |
|
}); |
|
} |
|
|
|
return map; |
|
} |
|
|
|
|
|
// create a new data object initialised with start location |
|
// |
|
function initData(coords) { |
|
return { |
|
paths: { |
|
"type": "GeometryCollection", |
|
"geometries": [] |
|
}, |
|
markers: { |
|
"type": "GeometryCollection", |
|
"geometries": [ |
|
{ |
|
"type" : "Point", |
|
"coordinates": [ coords.lng, coords.lat ] |
|
} |
|
] |
|
} |
|
}; |
|
} |
|
|
|
function getComputed(node) { |
|
return window.getComputedStyle(node); |
|
} |
|
|
|
function getElementDimensions(node) { |
|
var computed = getComputed(node); |
|
|
|
return { |
|
width: parseInt(computed.getPropertyValue("width"), 10), |
|
height: parseInt(computed.getPropertyValue("height"), 10) |
|
}; |
|
} |
|
|
|
})(this); |
|
</script> |
|
</body> |
|
</html> |