Copied entirely from https://www.mapbox.com/bites/00359/
forked from almccon's block: Mapbox routing demo (modified)
| license: mit |
Copied entirely from https://www.mapbox.com/bites/00359/
forked from almccon's block: Mapbox routing demo (modified)
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset='utf-8' /> | |
| <title>Fooder</title> | |
| <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' /> | |
| <script src='https://npmcdn.com/@turf/[email protected]/turf.min.js'></script> | |
| <script src="https://d3js.org/d3.v4.min.js"></script> | |
| <link href='https://www.mapbox.com/base/latest/base.css' rel='stylesheet' /> | |
| <script src='https://unpkg.com/[email protected]/cheap-ruler.js'></script> | |
| <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.38.0/mapbox-gl.js'></script> | |
| <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.38.0/mapbox-gl.css' rel='stylesheet' /> | |
| <style> | |
| body { margin:0; padding:0; } | |
| #map { | |
| position:absolute; | |
| top:0; | |
| bottom:0; | |
| right:0px; | |
| left:0px; | |
| background:black; | |
| } | |
| .truck{ | |
| margin:-10px -10px; | |
| } | |
| .truck, .ping { | |
| width:20px; | |
| height:20px; | |
| border:2px solid #fff; | |
| border-radius:50%; | |
| background:#3887be; | |
| pointer-events: none; | |
| } | |
| .ping { | |
| margin:-99999px; | |
| border:0.025px solid #f9886c; | |
| background:none; | |
| transform: translateX(-50%) translateY(-50%); | |
| } | |
| .ping.active{ | |
| margin:0px; | |
| z-index:99; | |
| transform:translateX(-50%) translateY(-50%) scale(6,6); | |
| opacity:0; | |
| transition: all 0.75s ease-out; | |
| transition-property: transform, opacity; | |
| } | |
| canvas:active { | |
| cursor:-webkit-grabbing; | |
| } | |
| .step{ | |
| padding:10px; | |
| border-bottom:1px solid #ddd; | |
| overflow:hidden; | |
| max-height:200px; | |
| text-align:left; | |
| color:#666; | |
| } | |
| .step:first-child { | |
| background:white; | |
| color:black; | |
| font-weight:bold; | |
| } | |
| .transient{ | |
| pointer-events: none; | |
| margin-top:-30px; | |
| } | |
| #phone { | |
| position: absolute; | |
| top: 15px; | |
| left: 15px; | |
| width: 250px; | |
| height: 460px; | |
| background: #fff; | |
| border-radius: 25px; | |
| z-index: 99; | |
| font-family: sans-serif; | |
| box-shadow: 0px 0px 15px #aaa; | |
| transition: all 0.2s; | |
| } | |
| #phone.hide{ | |
| transform: translate(-200%); | |
| } | |
| #screen{ | |
| width: 90%; | |
| height: 80%; | |
| margin: 15% auto; | |
| border-radius:8px; | |
| overflow:hidden; | |
| border:1px solid #ccc; | |
| position:relative; | |
| } | |
| #queue { | |
| background:#eee; | |
| height:35%; | |
| text-align:center; | |
| overflow-y: scroll; | |
| } | |
| #queue:empty{ | |
| padding:30px 10px; | |
| } | |
| #queue:empty:after{ | |
| content: 'Click and drag from a restaurant to add a new order for delivery'; | |
| opacity:0.5; | |
| } | |
| .foodicon{ | |
| height:20px; | |
| float:right; | |
| font-size:0.5em; | |
| color:#3887be; | |
| } | |
| .foodicon img{ | |
| width:20px; | |
| display:inline; | |
| float:right; | |
| margin-left:2px; | |
| } | |
| #nav { | |
| width:100%; | |
| height:65%; | |
| transition:all 0.5s; | |
| border-top-left-radius: 7px; | |
| border-top-right-radius: 7px; | |
| } | |
| #screen .mapboxgl-control-container{ | |
| display:none; | |
| } | |
| #carmarker{ | |
| z-index:99; | |
| position:absolute; | |
| width:50px; | |
| height:50px; | |
| border-radius:50%; | |
| background:#3887be; | |
| left:50%; | |
| top:50%; | |
| margin:-25px -25px; | |
| transform: rotateX(60deg); | |
| color: white; | |
| text-align: center; | |
| font-size: 3em; | |
| line-height: 1.2em; | |
| border:2px solid white; | |
| box-shadow: 0px 10px 25px #666; | |
| } | |
| /*Modal stuff */ | |
| .dragger img{ | |
| position:absolute; | |
| } | |
| #cursor{ | |
| width:20px; | |
| height:20px; | |
| margin:18px; | |
| } | |
| .drag { | |
| width: 60px; | |
| height: 60px; | |
| padding:10px; | |
| position: relative; | |
| overflow:visible; | |
| left:30%; | |
| } | |
| .dragger{ | |
| position:relative; | |
| animation: mymove 1.5s; | |
| animation-iteration-count: infinite; | |
| margin-top:-40px; | |
| } | |
| @keyframes mymove { | |
| 0% {transform: translateX(30%);opacity:0;} | |
| 20%{transform: translateX(30%);opacity:1; } | |
| 60% {transform: translateX(70%);} | |
| 80% {opacity:1;} | |
| 100% {transform: translateX(70%); opacity:0;} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id='phone'> | |
| <div id='screen'> | |
| <div id='queue' class='pin-bottom'></div> | |
| <div id='nav'> | |
| <div id='carmarker'>▲</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id='map' class='contain'> | |
| <div id='modal-content' class='animate modal modal-content active' style='background:rgba(0,0,0,0.75)'> | |
| <div class='col5 modal-body fill-white contain pad4' style='margin-top:15%'> | |
| <div class=' row3 clearfix'> | |
| <div class='prose prose-big center'> | |
| Click and drag from any restaurant to add a new order for delivery | |
| </div> | |
| <div style='border-radius:30px; border:2px solid #aaa; display:inline-block' class='drag space-top2' > | |
| <img src='https://www.mapbox.com/bites/00359/icons/pngs/pizza.png' style='width:60px'> | |
| </div> | |
| <div class='dragger space-left2'> | |
| <img src='https://www.mapbox.com/bites/00359/icons/bubble-pizza.svg'> | |
| <img src = 'https://www.mapbox.com/bites/00359/icons/cursor.png' id='cursor'> | |
| </div> | |
| </div> | |
| <div class=''> | |
| <div class='button space-top4 col12' onclick='d3.select(".modal-content").classed("active", false)'>Start simulation</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| var state = { | |
| loaded:{}, | |
| speedFactor: 50, | |
| pause: true, | |
| lastQueryTime: 0, | |
| truckLocation:[139.761168,35.693766], | |
| lastAtRestaurant:{ | |
| taco: 0, | |
| pizza: 0, | |
| noodles: 0 | |
| }, | |
| keepTrack:[], | |
| currentSchedule: [], | |
| currentRoute: null, | |
| pointHopper: {}, | |
| dragLine: [], | |
| ruler: cheapRuler(41.8949, 'meters'), | |
| //update map line geometry | |
| updateRoute: function(wholeRoute){ | |
| var payload; | |
| //state.updateNext(); | |
| if (!wholeRoute) payload = nothing; | |
| else{ | |
| var currentChunk = turf.lineSlice( | |
| turf.point(state.truckLocation), | |
| state.currentSchedule[0], | |
| wholeRoute.geometry | |
| ) | |
| var restOfLine = turf.lineSlice( | |
| state.currentSchedule[0], | |
| state.currentSchedule[state.currentSchedule.length-1], | |
| wholeRoute.geometry | |
| ); | |
| restOfLine.properties.current = 'false'; | |
| currentChunk.properties.current = 'true'; | |
| payload = turf.featureCollection([restOfLine,currentChunk]) | |
| } | |
| [map, driver] | |
| .forEach(function(mapObject){ | |
| mapObject | |
| .getSource('route') | |
| .setData(payload) | |
| }) | |
| }, | |
| //update queue display on phone | |
| updateQueue: function(){ | |
| var queue = state.currentSchedule | |
| .map(function(item){ | |
| return [item.properties.key, item.properties.type] | |
| }) | |
| d3.selectAll('.step') | |
| .remove(); | |
| d3.select('#queue') | |
| .selectAll('.step') | |
| .data(queue, function(d){return d}) | |
| .enter() | |
| .append('div') | |
| .classed('step', true) | |
| .text(function(d){ | |
| if (d[1]) return 'Deliver '+ d[1] | |
| else return 'Pick up '+ d[0] + ' order' | |
| }) | |
| .append('div') | |
| .classed('foodicon', true) | |
| .text(function(d){ | |
| var arrow = d[1] ? '▼' : '▲' | |
| return arrow | |
| }) | |
| .append('img') | |
| .attr('src', function(d){ | |
| var type = d[1] ? d[1] : d[0] | |
| return 'https://www.mapbox.com/bites/00359/icons/pngs/'+type+'.png' | |
| }) | |
| }, | |
| updatePickups: function(geojson){ | |
| [map, driver] | |
| .forEach(function(mapObject){ | |
| mapObject | |
| .getSource('pickups-symbol') | |
| .setData(geojson) | |
| }) | |
| }, | |
| // update next marker | |
| updateNext: function(){ | |
| var nextStop = state.currentSchedule[0] ? state.currentSchedule[0] : nothing; | |
| if (nextStop.properties && nextStop.properties.restaurant){ | |
| console.log('rest') | |
| restaurants.features.forEach(function(ft){ | |
| ft.properties.next = ft.properties.type === nextStop.properties.key | |
| console.log(ft.properties.next) | |
| }) | |
| map.getSource('restaurants') | |
| .setData(restaurants) | |
| } | |
| }, | |
| reset: function(){ | |
| console.log('resetting') | |
| state.pause = true; | |
| state.currentSchedule = state.keepTrack = []; | |
| state.pointHopper = {}; | |
| rests.forEach(function(rest){ | |
| state.pointHopper[rest.type] = turf.point(rest.coordinates, {key:rest.type}) | |
| }) | |
| //update the queue and route | |
| state.updateQueue(); | |
| if (state.loaded.map && state.loaded.driver){ | |
| map.getSource('next') | |
| .setData(nothing); | |
| state.updatePickups(nothing); | |
| state.updateRoute(); | |
| } | |
| }, | |
| onMousedown: function(e){ | |
| if (state.currentSchedule.length>=11) return | |
| var ft = map.queryRenderedFeatures(e.point, {layers:['restaurants']})[0] | |
| if (!ft) return; | |
| //hide phone | |
| d3.select('#phone') | |
| .classed('hide', true) | |
| // add transient marker to cursor | |
| var marker = document.createElement('div'); | |
| marker.innerHTML = '<img src="https://www.mapbox.com/bites/00359/icons/bubble-'+ft.properties.type+'.svg">' | |
| marker.classList = 'transient'; | |
| state.transientMarker = new mapboxgl.Marker(marker) | |
| .setLngLat(ft.geometry.coordinates) | |
| .addTo(map); | |
| //set drag guides | |
| map.getSource('dragHighlight') | |
| .setData(ft) | |
| state.dragLine[0] = ft.geometry.coordinates; | |
| map.setClasses(['dragging']); | |
| state.pause = true; | |
| state.updateRoute(); | |
| d3.select('.ping') | |
| .classed('active', false); | |
| map.once('mouseup', function(e){ | |
| state.onMouseup(e, ft) | |
| }) | |
| }, | |
| onMouseup: function(e, ft){ | |
| d3.select('#phone') | |
| .classed('hide', false); | |
| map.setClasses([]) | |
| .getSource('dragLine') | |
| .setData(nothing) | |
| d3.select('canvas') | |
| .style('cursor', null); | |
| state.newPickup(map.unproject(e.point), ft.properties.type); | |
| state.transientMarker.remove(); | |
| } | |
| } | |
| mapboxgl.accessToken = 'pk.eyJ1IjoicGV0ZXJxbGl1IiwiYSI6ImpvZmV0UEEifQ._D4bRmVcGfJvo1wjuOpA1g'; | |
| var embed = window.location.href.indexOf('embed=true') !==-1; | |
| var map = new mapboxgl.Map({ | |
| hash: false, | |
| scrollZoom: !embed, | |
| container: 'map', // container id | |
| style: 'mapbox://styles/peterqliu/cj5k2tj0d16ul2rmlwyw00gth', //stylesheet location | |
| center: state.truckLocation, // starting position | |
| zoom: 15 // starting zoom | |
| }); | |
| if(embed) map.addControl(new mapboxgl.NavigationControl()); | |
| var driver = new mapboxgl.Map({ | |
| container: 'nav', // container id | |
| style: 'mapbox://styles/peterqliu/cj5k3b63b176l2snvd7n6by90', //stylesheet location | |
| center: state.truckLocation, | |
| pitch:60, | |
| interactive: false, | |
| zoom: 17 // starting zoom | |
| }); | |
| var rests = [ | |
| {'type': 'taco', | |
| 'icon': '', | |
| 'coordinates': [139.759119,35.696143] | |
| }, | |
| {'type': 'pizza', | |
| 'icon': '', | |
| 'coordinates': [139.760318,35.696670] | |
| }, | |
| {'type': 'noodles', | |
| 'icon': '', | |
| 'coordinates': [139.761633,35.696040] | |
| }, | |
| ] | |
| var bbox = [ | |
| [-118.4053,34.0072], | |
| [-118.4253,34.1072] | |
| ] | |
| state.reset(); | |
| var nothing = turf.featureCollection([]); | |
| var restaurants = turf.featureCollection(rests.map(function(rest){ | |
| return turf.point(rest.coordinates, rest) | |
| })) | |
| var pickups = turf.featureCollection([]) | |
| driver.on('load', function(){ | |
| state.loaded.driver = true; | |
| this.addSource('route',{ | |
| 'data': nothing, | |
| 'type': 'geojson' | |
| }) | |
| driver.addLayer({ | |
| 'id': '3d-buildings', | |
| 'source': 'composite', | |
| 'source-layer': 'building', | |
| 'filter': ['==', 'extrude', 'true'], | |
| 'type': 'fill-extrusion', | |
| 'paint': { | |
| 'fill-extrusion-color': 'hsl(35, 28%, 70%)', | |
| 'fill-extrusion-height': { | |
| 'type': 'identity', | |
| 'property': 'height' | |
| }, | |
| 'fill-extrusion-base': { | |
| 'type': 'identity', | |
| 'property': 'min_height' | |
| }, | |
| 'fill-extrusion-opacity': .6 | |
| } | |
| }, 'waterway-label') | |
| .addLayer({ | |
| 'id':'routeline', | |
| 'type':'line', | |
| 'source':'route', | |
| 'layout':{ | |
| 'line-join':'round', | |
| 'line-cap':'round' | |
| }, | |
| 'paint':{ | |
| 'line-color': '#3887be', | |
| 'line-width': { | |
| base:1, | |
| stops:[[12,12],[22,16]] | |
| } | |
| } | |
| }, 'waterway-label') | |
| .addLayer({ | |
| 'id':'restaurants', | |
| 'type':'circle', | |
| 'source':{ | |
| 'data': restaurants, | |
| 'type': 'geojson' | |
| }, | |
| 'paint':{ | |
| 'circle-radius':{ | |
| 'property': 'next', | |
| 'type': 'categorical', | |
| 'default': 25, | |
| 'stops':[ | |
| [true, 27], | |
| [false, 25], | |
| ] | |
| }, | |
| 'circle-color': 'white', | |
| 'circle-stroke-color': { | |
| 'property': 'next', | |
| 'type': 'categorical', | |
| 'default': '#ccc', | |
| 'stops':[ | |
| [true, '#3887be'], | |
| [false, '#ccc'], | |
| ] | |
| }, | |
| 'circle-stroke-width': { | |
| 'property': 'next', | |
| 'type': 'categorical', | |
| 'default': 2, | |
| 'stops':[ | |
| [true, 3], | |
| [false, 2], | |
| ] | |
| } | |
| }, | |
| 'paint.dragging':{ | |
| 'circle-stroke-width': 0 | |
| } | |
| }) | |
| .addLayer({ | |
| 'id':'restaurants-symbol', | |
| 'type':'symbol', | |
| 'source':{ | |
| 'data': restaurants, | |
| 'type': 'geojson' | |
| }, | |
| 'layout':{ | |
| 'icon-image':'{type}', | |
| 'icon-size':0.125 | |
| }, | |
| 'paint':{ | |
| 'text-color': '#3887be' | |
| } | |
| }) | |
| .addLayer({ | |
| 'id':'pickups-symbol', | |
| //'filter':['has', 'orderTime'], | |
| 'type':'symbol', | |
| 'source':{ | |
| 'data': nothing, | |
| 'type': 'geojson' | |
| }, | |
| 'layout':{ | |
| 'icon-allow-overlap': true, | |
| 'icon-ignore-placement': true, | |
| 'icon-image':'bubble-{type}', | |
| //'text-field': '{index}' | |
| }, | |
| 'paint':{ | |
| 'text-translate':[-10,-30], | |
| 'icon-translate':[12,-15], | |
| 'text-color': '#3887be' | |
| } | |
| }) | |
| }) | |
| map.on('load', function(){ | |
| map.fitBounds(bbox) | |
| state.loaded.map = true; | |
| this.addSource('route',{ | |
| 'data': nothing, | |
| 'type': 'geojson' | |
| }) | |
| .addSource('next',{ | |
| 'data': nothing, | |
| 'type': 'geojson' | |
| }) | |
| this | |
| .addLayer({ | |
| 'id':'routearrows', | |
| 'type':'symbol', | |
| 'source':'route', | |
| 'filter':['==', 'current', 'true'], | |
| 'layout':{ | |
| 'symbol-placement': 'line', | |
| 'text-field': '▶', | |
| 'text-size':{ | |
| base:1, | |
| stops:[[12,24],[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': 'blackout', | |
| 'type':'background', | |
| 'paint':{ | |
| 'background-color': '#fff', | |
| 'background-opacity':0 | |
| }, | |
| 'paint.dragging':{ | |
| 'background-opacity':0.75 | |
| } | |
| }) | |
| .addLayer({ | |
| 'id':'dragLine', | |
| 'type':'line', | |
| 'source':{ | |
| 'data': nothing, | |
| 'type': 'geojson' | |
| }, | |
| 'layout':{ | |
| 'line-cap':'round' | |
| }, | |
| 'paint':{ | |
| 'line-width':0, | |
| 'line-width-transition':{ | |
| 'duration':0 | |
| } | |
| }, | |
| 'paint.dragging':{ | |
| 'line-color':'#f9886c', | |
| 'line-opacity':1, | |
| 'line-width': 2, | |
| 'line-dasharray':[0,4], | |
| } | |
| }) | |
| // .addLayer({ | |
| // 'id':'dragArrowhead', | |
| // 'type':'symbol', | |
| // 'source':{ | |
| // 'data': nothing, | |
| // 'type': 'geojson' | |
| // }, | |
| // 'layout':{ | |
| // 'text-cap':'round' | |
| // }, | |
| // 'paint':{ | |
| // 'text-opacity':0 | |
| // }, | |
| // 'paint.dragging':{ | |
| // 'text-color':'#f9886c', | |
| // 'text-opacity':1, | |
| // 'text-field': '>', | |
| // } | |
| // }) | |
| .addLayer({ | |
| 'id':'routeline-inactive', | |
| 'type':'line', | |
| 'source':'route', | |
| 'filter':['==', 'current', 'false'], | |
| 'layout':{ | |
| 'line-cap':'round' | |
| }, | |
| 'paint':{ | |
| 'line-color':'#3887be', | |
| 'line-opacity':0.8, | |
| 'line-width': { | |
| base:1, | |
| stops:[[12,3],[22,12]] | |
| }, | |
| 'line-dasharray':[0,2], | |
| } | |
| }, 'waterway-label') | |
| .addLayer({ | |
| 'id':'routeline-active', | |
| 'type':'line', | |
| 'source':'route', | |
| 'filter':['==', 'current', 'true'], | |
| 'layout':{ | |
| 'line-join':'round', | |
| 'line-cap':'round' | |
| }, | |
| 'paint':{ | |
| 'line-color':{ | |
| 'property': 'current', | |
| 'type': 'categorical', | |
| 'stops':[ | |
| ['true', '#3887be'], | |
| ['false', '#aaa'] | |
| ] | |
| }, | |
| 'line-width': { | |
| base:1, | |
| stops:[[12,3],[22,12]] | |
| } | |
| } | |
| }, 'waterway-label') | |
| .addLayer({ | |
| 'id':'restaurants', | |
| 'type':'circle', | |
| 'source':{ | |
| 'data': restaurants, | |
| 'type': 'geojson' | |
| }, | |
| 'paint':{ | |
| 'circle-radius':{ | |
| 'property': 'next', | |
| 'type': 'categorical', | |
| 'default': 25, | |
| 'stops':[ | |
| [true, 27], | |
| [false, 25], | |
| ] | |
| }, | |
| 'circle-color': 'white', | |
| 'circle-stroke-color': { | |
| 'property': 'next', | |
| 'type': 'categorical', | |
| 'default': '#ccc', | |
| 'stops':[ | |
| [true, '#3887be'], | |
| [false, '#ccc'], | |
| ] | |
| }, | |
| 'circle-stroke-width': { | |
| 'property': 'next', | |
| 'type': 'categorical', | |
| 'default': 2, | |
| 'stops':[ | |
| [true, 3], | |
| [false, 2], | |
| ] | |
| } | |
| }, | |
| 'paint.dragging':{ | |
| 'circle-stroke-width': 0 | |
| } | |
| }) | |
| .addLayer({ | |
| 'id':'dragHighlight', | |
| 'type':'circle', | |
| 'source':{ | |
| 'data': nothing, | |
| 'type': 'geojson' | |
| }, | |
| 'paint':{ | |
| 'circle-radius':25, | |
| 'circle-opacity':0 | |
| }, | |
| 'paint.dragging':{ | |
| 'circle-stroke-width':3, | |
| 'circle-stroke-color': '#f9886c' | |
| } | |
| }) | |
| .addLayer({ | |
| 'id':'restaurants-symbol', | |
| 'type':'symbol', | |
| 'source':{ | |
| 'data': restaurants, | |
| 'type': 'geojson' | |
| }, | |
| 'layout':{ | |
| 'icon-image':'{type}', | |
| 'text-font':['icomoon Regular'], | |
| 'icon-size':0.125 | |
| }, | |
| 'paint':{ | |
| 'text-color': '#3887be' | |
| } | |
| }) | |
| .addLayer({ | |
| 'id':'pickups-symbol', | |
| //'filter':['has', 'orderTime'], | |
| 'type':'symbol', | |
| 'source':{ | |
| 'data': nothing, | |
| 'type': 'geojson' | |
| }, | |
| 'layout':{ | |
| 'icon-allow-overlap': true, | |
| 'icon-ignore-placement': true, | |
| 'icon-image':'bubble-{type}', | |
| //'text-field': '{index}' | |
| }, | |
| 'paint':{ | |
| 'text-translate':[-10,-30], | |
| 'icon-translate':[12,-15], | |
| 'text-color': '#3887be' | |
| } | |
| }) | |
| var marker = document.createElement('div'); | |
| marker.classList = 'truck'; | |
| state.truckMarker = new mapboxgl.Marker(marker) | |
| .setLngLat(state.truckLocation) | |
| .addTo(map); | |
| state.pingMarker = document.createElement('div'); | |
| state.pingMarker.innerHTML = '<div class="ping"></div>'; | |
| state.pingMarker = new mapboxgl.Marker(state.pingMarker) | |
| .setLngLat(state.truckLocation) | |
| .addTo(map); | |
| map | |
| .on('mousemove', function(e){ | |
| var ft = map.queryRenderedFeatures(e.point, {layers:['restaurants']})[0] | |
| if (ft) map.dragPan.disable(); | |
| else map.dragPan.enable() | |
| if (e.originalEvent.which === 1){ | |
| var pt = [map.unproject(e.point).lng, map.unproject(e.point).lat]; | |
| state.transientMarker | |
| .setLngLat(pt); | |
| state.dragLine[1] = pt; | |
| map.getSource('dragLine') | |
| .setData(turf.lineString(state.dragLine)) | |
| } | |
| }) | |
| .on('mousedown', function(e){ | |
| state.onMousedown(e) | |
| }) | |
| }) | |
| function randomPoint(){ | |
| var pt = turf.random('points', 1,{bbox:bbox}).features[0].geometry.coordinates | |
| var randomNumber = parseFloat((Math.random()*100).toFixed(0))%3; | |
| var randomType = rests[randomNumber].type; | |
| state.newPickup({lng:pt[0], lat:pt[1]}, randomType) | |
| }; | |
| state.newPickup = function (coords, type){ | |
| //state.pingMarker._element.classList ='ping mapboxgl-marker'; | |
| d3.select('.ping') | |
| .classed('active', false) | |
| var pt = addPoint(coords, type); | |
| state.pointHopper[pt.properties.key] = pt; | |
| pickups.features.push(pt); | |
| state.pingMarker | |
| .setLngLat(pt.geometry.coordinates) | |
| d3.select('.ping') | |
| .classed('active', true) | |
| console.log(assembleQueryURL()); | |
| d3.json(assembleQueryURL(), function(err, resp){ | |
| state.currentRoute = resp.trips[0]; | |
| state.pause = false; | |
| // draw the line except the last step (no need for round trip) | |
| var lastStep = resp.trips[0].legs[resp.trips[0].legs.length-1].steps[0].intersections[0].location | |
| var slicedLine = turf.lineSlice( | |
| turf.point(state.currentRoute.geometry.coordinates[0]), | |
| lastStep, | |
| state.currentRoute.geometry | |
| ) | |
| state.currentRoute.geometry = slicedLine; | |
| state.lastQueryTime = Date.now(); | |
| var timeSoFar = Date.now(); | |
| state.currentSchedule = []; | |
| for(var i =1; i<resp.waypoints.length; i++){ | |
| var legTime = resp.trips[0].legs[i].duration; | |
| timeSoFar += legTime*1000/state.speedFactor; | |
| // snap pickups to the road grid, and edit entries accordingly | |
| state.keepTrack[i].geometry.coordinates = resp.waypoints[i].location; | |
| var waypointIndex = resp.waypoints[i]['waypoint_index']; | |
| state.currentSchedule[waypointIndex] = state.keepTrack[i]; | |
| } | |
| state.currentSchedule.shift(); | |
| state.updatePickups( | |
| turf.featureCollection( | |
| state.currentSchedule.map( | |
| function(ft, index){ | |
| ft.properties.index = index; | |
| return ft | |
| } | |
| ) | |
| ) | |
| ) | |
| var nextDestination = state.pointHopper[state.currentSchedule[0].properties.key]; | |
| nextDestination.properties.restaurant = !nextDestination.properties.orderTime | |
| tick(); | |
| state.updateQueue(); | |
| state.updateRoute(slicedLine); | |
| }) | |
| } | |
| function addPoint(coords, type){ | |
| var point = turf.point( | |
| [coords.lng, coords.lat], | |
| { | |
| type: type, | |
| orderTime: Date.now(), | |
| key: Math.random() | |
| } | |
| ) | |
| return point | |
| } | |
| function tick(){ | |
| if (!state.pause){ | |
| moveTruck() | |
| requestAnimationFrame(tick) | |
| } | |
| } | |
| function moveTruck(){ | |
| // update truck position | |
| var timeElapsed = (Date.now()-state.lastQueryTime)/(1000/state.speedFactor); | |
| var timeRatio = timeElapsed/state.currentRoute.duration | |
| var progressDistance = state.currentRoute.distance * timeRatio; | |
| var newSpot = state.ruler.along(state.currentRoute.geometry.geometry.coordinates, progressDistance); | |
| var bearing = state.ruler.bearing(state.truckLocation, newSpot) | |
| state.truckLocation = newSpot; | |
| state.truckMarker | |
| .setLngLat(state.truckLocation); | |
| driver.setCenter(state.truckLocation) | |
| .easeTo({bearing:bearing, duration:200}) | |
| //check if truck has reached the next waypoint | |
| var currentStep = state.currentSchedule[0]; | |
| if(currentStep && state.ruler.distance(state.truckLocation, currentStep.geometry.coordinates)<10){ | |
| var key = currentStep.properties.key; | |
| state.currentSchedule.shift(); | |
| //update route line | |
| var slicedLine = turf.lineSliceAlong( | |
| state.currentRoute.geometry, | |
| progressDistance/1000, | |
| Infinity, | |
| 'kilometers' | |
| ) | |
| pickups = turf.featureCollection( | |
| state.currentSchedule.filter(function(spot){ | |
| return spot.properties.key !== key | |
| }) | |
| ) | |
| // if this location is a dropoff, remove it from the hopper and update map markers | |
| if (typeof key === 'number') { | |
| delete state.pointHopper[currentStep.properties.key]; | |
| pickups = turf.featureCollection( | |
| objectToArray(state.pointHopper) | |
| .filter(function(pt){ | |
| return typeof pt.properties.key === 'number' | |
| }) | |
| ) | |
| state.updatePickups(pickups) | |
| } | |
| //if it's a restaurant, update the arrival time | |
| else state.lastAtRestaurant[key] = Date.now(); | |
| //if no more deliveries, pause; | |
| if (state.currentSchedule.length === 0) { | |
| state.pause = true; | |
| map.getSource('next') | |
| .setData(nothing) | |
| state.updateRoute() | |
| } | |
| else { | |
| state.updateRoute(slicedLine); | |
| var nextDestination = state.pointHopper[state.currentSchedule[0].properties.key] | |
| nextDestination.properties.restaurant = !nextDestination.properties.orderTime | |
| map.getSource('next') | |
| .setData(nextDestination); | |
| } | |
| state.updateQueue() | |
| } | |
| } | |
| function assembleQueryURL(){ | |
| var coordinates = [state.truckLocation]; | |
| var distributions = []; | |
| state.keepTrack = [state.truckLocation]; | |
| rests.forEach(function(rest, index){ | |
| var restJobs = objectToArray(state.pointHopper) | |
| .filter(function(job){ | |
| return job.properties.type === rest.type; | |
| }) | |
| // if there are actually orders from this restaurant | |
| if (restJobs.length > 0){ | |
| var needToPickUp = restJobs.filter(function(d,i){ | |
| return d.properties.orderTime > state.lastAtRestaurant[rest.type] | |
| }).length>0; | |
| if (needToPickUp) { | |
| var restaurantIndex = coordinates.length; | |
| coordinates.push(rest.coordinates); | |
| // push the restaurant itself into the array | |
| state.keepTrack.push(state.pointHopper[rest.type]); | |
| } | |
| restJobs.forEach(function(d,i){ | |
| //add pickup to list | |
| state.keepTrack.push(d); | |
| coordinates.push(d.geometry.coordinates); | |
| // if order not yet picked up, add a reroute | |
| if (needToPickUp && d.properties.orderTime>state.lastAtRestaurant[rest.type]){ | |
| distributions.push(restaurantIndex+','+(coordinates.length-1)) | |
| } | |
| }) | |
| } | |
| }); | |
| return ('https://api.mapbox.com/optimized-trips/v1/mapbox/driving/'+coordinates.join(';')+'?distributions='+distributions.join(';')+'&overview=full&steps=true&geometries=geojson&source=first&access_token='+mapboxgl.accessToken) | |
| } | |
| function objectToArray(obj){ | |
| var keys = Object.keys(obj); | |
| var payload = keys.map(function(key){ | |
| return obj[key] | |
| }) | |
| return payload | |
| } | |
| window.addEventListener('blur', function(){state.reset()}) | |
| </script> | |
| <img src='https://www.mapbox.com/bites/00359/icons/bubble-noodles.svg'> | |
| <img src='https://www.mapbox.com/bites/00359/icons/bubble-taco.svg'> | |
| </body> | |
| </html> | |