Skip to content

Instantly share code, notes, and snippets.

@eltonjuan
Created November 18, 2017 21:05
Show Gist options
  • Save eltonjuan/ca229664e65ed6c1a30f77e5a8d16fe7 to your computer and use it in GitHub Desktop.
Save eltonjuan/ca229664e65ed6c1a30f77e5a8d16fe7 to your computer and use it in GitHub Desktop.
mapboxgl.accessToken = 'pk.eyJ1IjoicGV0ZXJxbGl1IiwiYSI6ImpvZmV0UEEifQ._D4bRmVcGfJvo1wjuOpA1g';
var state = {
freePan: true,
sidebarMode: 'query',
startingPosition:[-122.337856, 47.607294],
mode:'walking',
locationString:'seattle',
timeViewZoom:13,
throttleDuration: 500,
timeMarkers:[],
timeLabel:undefined,
directions: undefined,
ruler: function(){
return cheapRuler(state.startingPosition[1], 'meters')
},
lastQueryTime: Date.now(),
baseIconEquivalent:{
walking: 'walk',
cycling: 'bike',
driving: 'car'
},
locations:{
physical: turf.featureCollection([]),
time: turf.featureCollection([])
},
query:'sandwich',
queryGeocoder: function(cb){
var query = state.locationString.replace(' ', '+');
var queryURL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'
+ query
+ '.json?access_token='+ mapboxgl.accessToken;
d3.json(queryURL, function(err, resp){
state.startingPosition = resp.features[0].center;
state.queryFoursquare();
setBackground();
})
},
queryFoursquare: function(cb){
var coords = state.startingPosition;
var queryURL = 'https://api.foursquare.com/v2/venues/search?query='+state.query+'&limit=50&radius=50000&intent=browse&ll='+[coords[1], coords[0]]+'&client_id=MZDCFZGD22TREAJNG54DXPIBJXQPLQKAB54FPOQD0QKUPKWP&client_secret=1LJRQTLGZ31HL5YSKJCZNGJYOESNOOBBJDDWXA4VU5JTPZ2D&v=20171111';
d3.json(queryURL, function (err, resp){
var venues = resp.response.venues;
var geojson = venues.map(function(venue){
var properties = {
'name': venue.name,
'url': venue.url,
'address': venue.location.address
}
if (venue.categories[0]) {
properties.icon = venue.categories[0].icon.prefix;
}
return turf.point([venue.location.lng, venue.location.lat], properties)
})
state.locations.physical = turf.featureCollection(geojson);
//get travel times
state.queryDuration(function(err, resp){
state.updateMarkers(resp);
})
})
},
queryDuration: function(cb){
var coords = state.locations.physical.features.map(
function(item){
return item.geometry.coordinates
}
);
var queryURL = 'https://api.mapbox.com/directions-matrix/v1/mapbox/'+state.mode+'/'+state.startingPosition+';'+coords.join(';')+'?sources=0&destinations=all&access_token='+ mapboxgl.accessToken
d3.json(queryURL, cb)
},
queryDirections: function(coords, d){
d.geometry.coordinates = d.properties.physicalLocation;
map.getSource('locations')
.setData(d);
var strung = coords.join(';')
var queryURL = 'https://api.mapbox.com/directions/v5/mapbox/'+state.mode+'/'+strung+'?geometries=geojson&steps=true&access_token='+ mapboxgl.accessToken;
d3.json(queryURL, function(err, resp){
state.directions = resp.routes[0];
state.drawDirections(coords);
state.updateSidebar('venue', resp);
})
},
drawDirections: function(endpoints){
//draw line to map
var line = (state.directions.geometry.coordinates);
line.push(endpoints[1]);
line
.unshift(endpoints[0])
line = turf.lineString(line)
map.getSource('route')
.setData(line);
map.setClasses(['roads'])
//set the timelabel marker
var midpointDistance = state.directions.distance/2;
var midpoint = turf.along(turf.lineString(state.directions.geometry.coordinates), midpointDistance/1000, 'kilometers');
document.querySelector('#duration')
.innerHTML = (state.directions.duration/60).toFixed(0)+' min'
state.timeLabel
.setLngLat(midpoint.geometry.coordinates)
//toggle map object class to fade out timelabels
d3.select('#timemap')
.classed('roads', true)
// zoom in to frame the linestring properly
var bbox = line.geometry.coordinates
.reduce(function(bounds, coord) {
return bounds.extend(coord);
}, new mapboxgl.LngLatBounds(state.startingPosition, state.startingPosition));
map.fitBounds(bbox, {
padding: {left:60, right:60, top:60, bottom:60},
duration: 500
});
},
updateSidebar: function(viewType, item){
d3.select('#sidebar')
.attr('class', viewType+' scroll-styled z100');
if( viewType === 'venue'){
var data = (item.routes[0].legs[0].steps)
d3.selectAll('.step')
.remove();
var instructions = d3.select('#instructions')
.selectAll('.step')
.data(data)
.enter()
.append('div')
.attr('class', 'step pad1 keyline-bottom');
instructions
.append('div')
.attr('class', 'fr prose prose-big center')
.text(function(d){
if (d.duration === 0) return
else {
var seconds = Math.round(d.duration % 60);
seconds = seconds< 10 ? '0' + seconds : seconds;
return Math.floor(d.duration/60) + ':' + seconds;
}
})
// maneuver instruction
instructions
.append('div')
.attr('class','instruction')
.text(function(step){
var abbr = step.maneuver.instruction
.replace('Avenue', 'Ave')
.replace('Place', 'Pl')
.replace('Street', 'St')
.replace('Boulevard', 'Blvd')
return abbr
});
//distance reading
instructions
.append('div')
.attr('class', 'quiet small')
.text(function(d){
var string = d.distance > 400 ? (d.distance/1609).toFixed(1) + ' mi' : Math.round(d.distance*3.28084) + ' ft'
return string
})
}
else {
var sorted = state.locations.time.features
.sort(function(x,y){
return d3.ascending(x.properties.duration, y.properties.duration);
})
d3.selectAll('.venuelisting')
.remove();
var venues = d3.select('#results')
.selectAll('.venuelisting')
.data(sorted)
.enter()
.append('div')
.attr('class', 'venuelisting pad1 keyline-bottom')
.on('click', function(d){
var coords = [state.startingPosition, d.properties.physicalLocation];
state.queryDirections(coords, d);
});
venues
.append('div')
.attr('class', 'fr prose prose-big center time')
.text(function(d){
return Math.round(d.properties.duration/60)
})
venues
.append('div')
.attr('class', 'strong truncate')
.text(function(d){
return d.properties.name
})
venues
.append('div')
.attr('class', 'quiet small')
.text(function(d){
var address = d.properties.address || 'Address unlisted'
return address
})
}
},
updateMarkers: function(resp){
state.locations.time.features = [];
var durations = resp.durations[0];
state.locations.physical.features.forEach(function(d,i){
var bearing = (turf.bearing(turf.point(state.startingPosition), d))
var pt = turf.destination(turf.point(state.startingPosition), durations[i+1]/60/10, bearing, 'kilometers');
pt.properties = d.properties
pt.properties.duration = durations[i+1];
pt.properties.textOrientation = bearing<0 ? 'rightText' : 'leftText';
pt.properties.bearing = bearing+180;
pt.properties.physicalLocation = d.geometry.coordinates;
state.locations.time.features.push(pt)
})
map.getSource('locations')
.setData(state.locations.physical)
if (state.timeMarkers.length === 0){
state.timeMarkers = state.locations.time.features.map(function(item){
var el = document.createElement('div');
el.classList+= ' timemarker';
var truncatedName = item.properties.name.length>30 ? item.properties.name.substr(0,27)+'...' : item.properties.name;
el.innerHTML = '<img class="circle inline" src="'+item.properties.icon+'64.png"><div class="locationName '+
item.properties.textOrientation+'"><div class="vcenter">'+truncatedName+'</div>';
var marker = new mapboxgl.Marker(el);
marker
.setLngLat(item.geometry.coordinates)
.addTo(map);
//bind click handler to each marker
var coordinates = [state.startingPosition, item.properties.physicalLocation];
//when clicked, get directions for venue, and update sidebar in parallel
marker.getElement().addEventListener('click', function(){
state.queryDirections(coordinates, item)
})
return marker;
})
}
else {
state.timeMarkers.forEach(function(marker, index){
d3.select('#timemap')
.classed('animating', true);
map.once('movestart', function(){
d3.select('#timemap')
.classed('animating', false);
})
marker
.setLngLat(state.locations.time.features[index].geometry.coordinates)
})
}
state.updateSidebar('query')
},
newSearch: function(property, value){
state[property] = value;
if (property === 'mode'){
//get travel times
state.queryDuration(function(err, resp){
state.updateMarkers(resp);
});
d3.select('.timelabel .icon')
.attr('class', function(){
return 'icon blue '+ state.baseIconEquivalent[state.mode]
})
}
else {
state.lastQueryTime = Date.now();
if (property === 'query') {
window.setTimeout(
function(){
throttler(state.queryFoursquare)
},
state.throttleDuration
);
}
if (property === 'locationString'){
window.setTimeout(
function(){
throttler(state.queryGeocoder);
setBackground();
},
state.throttleDuration
);
}
}
function throttler(fn, cb){
if (Date.now() < state.lastQueryTime+state.throttleDuration) return;
//remove old markers
state.timeMarkers.forEach(function(marker){
marker.remove();
})
state.timeMarkers = [];
fn(cb)
}
}
};
var map = new mapboxgl.Map({
container: 'timemap', // container id
style: 'mapbox://styles/mapbox/light-v8', //stylesheet location
dragPan: state.freePan,
scrollZoom: state.freePan,
minZoom:11,
center: state.startingPosition, // starting position
zoom: state.timeViewZoom // starting zoom
});
var mapObjs = [map];
if (!state.freePan){
var ids =['#timemap'];
ids.forEach(function(id, i){
var object = mapObjs[i];
document.querySelector(id)
.addEventListener('mousewheel', function(e){
var z = object.getZoom()-e.deltaY/100
object.setZoom(z)
})
})
}
function pointBuffer (pt, radius, units, resolution) {
var buffer = turf.circle(turf.point(pt), radius, resolution, 'kilometers');
return buffer
}
function setBackground(){
var circles = [];
var labelRotations = [0, 60, -60, 0, 60, -60];
for (var s=120; s>=1; s--){
var circle = pointBuffer(state.startingPosition, s/10, 'kilometers',180);
// circles every 10 and 30 minutes (minor and major, respectively)
if (s%10 === 0) {
var primacy = s%30 === 0 ? 'major' : 'minor';
var suffix = s%30 === 0 ? ' MIN' : '';
circle.properties.primacy = primacy;
circle.properties.label = s;
circles.push(circle)
for (var i=0; i<360; i+=60){
var label = turf.destination(turf.point(state.startingPosition), s/10, i, 'kilometers');
label.properties = {
'label': s,
'size': primacy,
'suffix': suffix,
'rotation': labelRotations[i/60]
}
circles.push(label)
//guide lines
if (s==120 && i<360){
var line = [label.geometry.coordinates];
line.push(state.startingPosition)
circles.push(turf.lineString(line, {label:s, primacy: 'guide'}))
}
}
}
//small circles (per minute)
else {
circle.properties.label = s%10;
circle.properties.primacy = 'tick';
circles.push(circle)
}
}
map.getSource('circles')
.setData(turf.featureCollection(circles));
map.getSource('origin')
.setData(turf.point(state.startingPosition));
map.setCenter(state.startingPosition);
}
map.on('load', function(){
state.newSearch('query', 'sandwich')
var el = document.createElement('div');
el.classList+= ' timelabel strong pad0 bg-blue dark';
el.innerHTML = '<span id="duration" class=""></span>';
var marker = new mapboxgl.Marker(el)
.setLngLat(state.startingPosition)
.addTo(map);
state.timeLabel = marker;
map
.addSource('locations',
{
type:'geojson',
data: state.locations.time
}
)
.addSource('route',
{
type:'geojson',
data: turf.featureCollection([])
}
)
.addSource('circles',
{
type:'geojson',
data: turf.featureCollection([])
}
)
map
.addLayer({
'id': 'cover',
'type':'background',
'paint':{
'background-color': '#fff',
'background-opacity':0.99
},
'paint.roads':{
'background-opacity': 0
}
})
.addLayer({
'id':'routearrows',
'type':'symbol',
'source':'route',
'layout':{
'symbol-placement': 'line',
'text-field': '▶',
'text-size':{
base:1,
stops:[[12,18],[22,60]]
},
'symbol-spacing': {
base:1,
stops:[[12,30],[22,160]]
},
'text-keep-upright': false
},
'paint':{
'text-color': '#3887be',
'text-halo-color':'hsl(55, 11%, 96%)',
'text-halo-width':3
}
}, 'waterway-label')
.addLayer({
'id': 'route',
'type':'line',
'source':'route',
'paint':{
'line-width':{
'base':1,
'stops':[[10,1],[16,5]]
},
'line-color': '#3887be'
}
}, 'cover')
.addLayer({
'id':'guide-lines',
'type':'line',
'source':'circles',
'filter':['==', 'primacy', 'guide'],
'layout':{
'line-cap':'round'
},
'paint':{
'line-color':'#4897ce',
'line-dasharray':[0,6],
'line-width':{
'base':1,
'stops':[[10,0.25],[16,2]]
}
},
'paint.roads':{
'line-opacity':0
}
})
.addLayer({
'id':'circles-tick',
'type':'line',
'source':'circles',
'filter':['==', 'primacy', 'tick'],
'paint':{
'line-color':{
"property": "label",
'type': 'exponential',
'base':0.95,
"stops": [
[-3, '#3887be'],
[15, '#fff'],
]
},
'line-width':{
'base':0.75,
'stops':[[9,0],[16,0.5]]
}
},
'paint.roads':{
'line-opacity':0
}
})
.addLayer({
'id':'circles-major ',
'type':'line',
'source':'circles',
'filter':['in', 'primacy', 'major', 'minor'],
'layout':{
'line-cap':'round'
},
'paint':{
'line-color':{
"property": "label",
'type': 'exponential',
'base':0.99,
"stops": [
[0, '#3887be'],
[300, '#fff']
]
},
'line-width':{
'base':1,
'stops':[[6,1],[18,1]]
},
},
'paint.roads':{
'line-opacity':0
}
})
.addLayer({
'id':'timelabel',
'type':'symbol',
'filter':["==", "$type", "Point"],
'source':'circles',
'layout':{
'text-padding':1,
'text-size': {
"property": "size",
"type": "categorical",
"stops": [
[{zoom: 10, value: 'minor'}, 0],
[{zoom: 10, value: 'major'}, 12],
[{zoom: 12, value: 'minor'}, 14],
[{zoom: 12, value: 'major'}, 16]
]
},
'text-field': '{label}{suffix}',
'text-font': ['Open Sans Regular'],
'text-rotate': {
"property": "rotation",
"type": "identity",
}
},
'paint':{
'text-color':{
"property": "size",
"type": "categorical",
"stops": [
['minor', '#4897ce'],
['major', '#3887be']
]
},
'text-opacity':1,
'text-halo-color':{
"property": "label",
'type': 'exponential',
'base':0.99,
"stops": [
[0, '#fff'],
[300, '#fff']
]
},
'text-halo-width':2
},
'paint.roads':{
'text-opacity':0
}
})
.addLayer({
'id':'locationtext',
'type':'symbol',
'source':'locations',
'layout':{
'text-field':'{name}',
'text-size': 12,
'text-offset': {
'property':'bearing',
'type':'interval',
'stops':[[0,[-1,0]], [180, [1,0]]]
},
'text-justify':{
'property':'bearing',
'type':'interval',
'stops':[[0,'right'], [180, 'left']]
},
'text-optional': true,
'text-padding':5,
'text-font': ['Open Sans Regular'],
'text-anchor':{
'property':'bearing',
'type':'interval',
'stops':[[0,'right'], [180, 'left']]
}
},
'paint':{
'text-color':'#333',
'text-halo-color':'#fff',
'text-halo-width':2,
'text-opacity':0,
'icon-opacity':0
},
'paint.roads':{
'text-opacity':1,
'icon-opacity':1
}
}, 'cover')
.addLayer({
'id':'locationdot',
'type':'circle',
'source':'locations',
'paint':{
'circle-opacity':0,
'circle-stroke-opacity':0,
'circle-color': '#3887be',
'circle-stroke-color':'#fff',
'circle-stroke-width':{
'base':1,
'stops':[[10,1],[18,3]]
},
'circle-radius':{
'base':1,
'stops':[[10,1],[18,8]]
},
//'circle-opacity':0.25
},
'paint.roads':{
'circle-opacity':1,
'circle-stroke-opacity':1
}
},'cover')
.addLayer({
'id':'origin',
'type':'circle',
'source':{
type:'geojson',
data: turf.point(state.startingPosition)
},
'paint':{
'circle-color':'#fff',
'circle-stroke-color':'#3887be',
'circle-stroke-width':3,
'circle-radius':6
}
})
.addLayer({
'id':'roaddot',
'type':'circle',
'source':{
type:'geojson',
data: turf.featureCollection([])
},
'paint':{
'circle-color':'red',
'circle-stroke-color':'#ddd',
'circle-stroke-width':1,
'circle-radius':10
}
})
.addLayer({
'id':'roadpath',
'type':'line',
'source':{
type:'geojson',
data: turf.featureCollection([])
},
'paint':{
'line-color':'red',
'line-width':2
}
})
setBackground();
d3.select('#toggle').on('click', function(e){
var forward = map.getClasses().length === 0;
map.setClasses((forward ? ['roads'] : []));
d3.select('#timemap')
.classed('roads', forward);
map.flyTo({zoom:state.timeViewZoom,center:state.startingPosition})
})
d3.select('#timemap')
.classed('loading', false);
})
function getLocation() {
if (navigator.geolocation) {
document.querySelector('#geocoder').setAttribute('value','Getting your location...')
navigator.geolocation.getCurrentPosition(showPosition);
d3.select('#timemap')
.classed('loading', true);
function showPosition(position) {
//remove old markers
state.timeMarkers.forEach(function(marker){
marker.remove();
})
state.timeMarkers = [];
state.startingPosition = [position.coords.longitude, position.coords.latitude];
state.queryFoursquare();
d3.select('#timemap')
.classed('loading', false);
setBackground();
document.querySelector('#geocoder')
.setAttribute('value','Current location');
}
} else {
alert("Geolocation is not supported by this browser.");
}
}
//getLocation()
//wire up sidebar functionality
var mode = d3.selectAll('.mode a')
.data(['walk', 'bike', 'car'])
.attr('class', function(d,i){
var active = i===0 ? 'active' : ''
return active + ' icon col4 center '+ d
})
mode
.on('click', function(d,i){
mode
.classed('active', function(){
return d3.select(this).classed(d)
})
var modeTerms = ['walking', 'cycling', 'driving'];
state.newSearch('mode', modeTerms[i])
})
var centerLock = d3.selectAll('.centerlock a')
.data([false, true])
.attr('class', function(d,i){
var active = i===0 ? 'active' : ''
return active + ' center '+ d
})
centerLock
.on('click', function(d,i){
centerLock
.classed('active', function(){
return d3.select(this).classed(d)
})
state.freePan = d
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment