├── .gitignore ├── LICENSE ├── README.md ├── css ├── OpenSans.css ├── fonts │ ├── OpenSans-Bold.ttf │ ├── OpenSans-BoldItalic.ttf │ ├── OpenSans-ExtraBold.ttf │ ├── OpenSans-ExtraBoldItalic.ttf │ ├── OpenSans-Italic.ttf │ ├── OpenSans-Light.ttf │ ├── OpenSans-LightItalic.ttf │ ├── OpenSans-Regular.ttf │ ├── OpenSans-Semibold.ttf │ └── OpenSans-SemiboldItalic.ttf └── vroom.css ├── images ├── fa-icons │ ├── chevron-left.svg │ ├── chevron-right.svg │ ├── cog-alt.svg │ ├── delete.svg │ ├── resize-small.svg │ └── trash.svg ├── throbber.gif └── vroom.svg ├── index.html ├── package.json ├── scripts └── dist.sh └── src ├── config ├── api.js └── leaflet_setup.js ├── controls ├── clear.js ├── collapse.js ├── fit.js ├── panel.js ├── solve.js └── summary.js ├── data.js ├── map.js └── utils ├── address.js ├── data_handler.js ├── file_handler.js ├── geocoder.js ├── locations.js ├── overpass.js ├── solution_handler.js └── timing.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bundle*js 3 | dist 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Julien Coupey 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo contains the VROOM frontend demo hosted at 2 | [http://map.vroom-project.org/](http://map.vroom-project.org/). 3 | 4 | # Setup 5 | 6 | Clone the repo and install dependencies using `npm`: 7 | 8 | ```bash 9 | git clone https://github.com/VROOM-Project/vroom-frontend.git 10 | cd vroom-frontend 11 | npm install 12 | ``` 13 | 14 | # Requirements 15 | 16 | To run the frontend locally, you need the following tools up and 17 | running. 18 | 19 | - [OSRM](https://github.com/Project-OSRM/osrm-backend/wiki/Building-OSRM) 20 | v5.0.0 or later. 21 | - [VROOM](https://github.com/VROOM-Project/vroom/wiki/Building) v1.0.0 22 | or later 23 | - [vroom-express](https://github.com/VROOM-Project/vroom-express) to 24 | expose VROOM's API over http requests. 25 | 26 | # Usage 27 | 28 | Serve at `http://127.0.0.1:9966` with: 29 | 30 | ```bash 31 | npm run serve 32 | ``` 33 | -------------------------------------------------------------------------------- /css/OpenSans.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-style: italic; 4 | font-weight: 400; 5 | src: local('Open Sans Italic'), 6 | url('./fonts/OpenSans-Italic.ttf') format('truetype'); 7 | } 8 | 9 | @font-face { 10 | font-family: 'Open Sans'; 11 | font-style: normal; 12 | font-weight: 700; 13 | src: local('Open Sans Bold'), 14 | url('./fonts/OpenSans-Bold.ttf') format('truetype'); 15 | } 16 | 17 | @font-face { 18 | font-family: 'Open Sans'; 19 | font-style: normal; 20 | font-weight: 400; 21 | src: local('Open Sans Regular'), 22 | url('./fonts/OpenSans-Regular.ttf') format('truetype'); 23 | } -------------------------------------------------------------------------------- /css/fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VROOM-Project/vroom-frontend/2f5484661685dd383e6c5a914f8527b14a8eb774/css/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /css/fonts/OpenSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VROOM-Project/vroom-frontend/2f5484661685dd383e6c5a914f8527b14a8eb774/css/fonts/OpenSans-BoldItalic.ttf -------------------------------------------------------------------------------- /css/fonts/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VROOM-Project/vroom-frontend/2f5484661685dd383e6c5a914f8527b14a8eb774/css/fonts/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /css/fonts/OpenSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VROOM-Project/vroom-frontend/2f5484661685dd383e6c5a914f8527b14a8eb774/css/fonts/OpenSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /css/fonts/OpenSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VROOM-Project/vroom-frontend/2f5484661685dd383e6c5a914f8527b14a8eb774/css/fonts/OpenSans-Italic.ttf -------------------------------------------------------------------------------- /css/fonts/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VROOM-Project/vroom-frontend/2f5484661685dd383e6c5a914f8527b14a8eb774/css/fonts/OpenSans-Light.ttf -------------------------------------------------------------------------------- /css/fonts/OpenSans-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VROOM-Project/vroom-frontend/2f5484661685dd383e6c5a914f8527b14a8eb774/css/fonts/OpenSans-LightItalic.ttf -------------------------------------------------------------------------------- /css/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VROOM-Project/vroom-frontend/2f5484661685dd383e6c5a914f8527b14a8eb774/css/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /css/fonts/OpenSans-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VROOM-Project/vroom-frontend/2f5484661685dd383e6c5a914f8527b14a8eb774/css/fonts/OpenSans-Semibold.ttf -------------------------------------------------------------------------------- /css/fonts/OpenSans-SemiboldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VROOM-Project/vroom-frontend/2f5484661685dd383e6c5a914f8527b14a8eb774/css/fonts/OpenSans-SemiboldItalic.ttf -------------------------------------------------------------------------------- /css/vroom.css: -------------------------------------------------------------------------------- 1 | #map{ 2 | font-family: 'Open Sans'; 3 | background: #eee; 4 | position: absolute; 5 | width: 100%; 6 | height: 100%; 7 | top: 0; 8 | right: 0; 9 | bottom: 0; 10 | left: 0; 11 | } 12 | 13 | .panel-header{ 14 | padding: 20px 10px; 15 | } 16 | 17 | .panel-header a{ 18 | text-decoration: none; 19 | outline: none; 20 | } 21 | 22 | .panel-control{ 23 | background-color: #fff; 24 | opacity: 0.9; 25 | bottom: 20px; 26 | overflow-x: auto; 27 | padding: 0 20px 40px; 28 | position: fixed; 29 | right: 0px; 30 | top: 0px; 31 | z-index: 10; 32 | border-radius: 4px; 33 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); 34 | max-width: 300px; 35 | min-width: 200px; 36 | width: 33.3333%; 37 | margin: 0 !important; 38 | } 39 | 40 | .panel-table{ 41 | font-size: 14px; 42 | cursor: pointer; 43 | top: 0; 44 | right: 0; 45 | overflow: auto; 46 | } 47 | 48 | .panel-vehicle{ 49 | width: 100%; 50 | } 51 | 52 | .delete-location{ 53 | background-repeat: no-repeat; 54 | background-size: auto auto; 55 | background-position: center; 56 | width: 24px; 57 | height: 18px; 58 | margin-top: 0; 59 | padding: 0px 0px; 60 | background-image: url("../images/fa-icons/delete.svg"); 61 | } 62 | 63 | .clone-vehicle{ 64 | font-size: 14px; 65 | font-weight: bold; 66 | float:right; 67 | cursor: pointer; 68 | } 69 | 70 | .custom-control{ 71 | background: rgba(255,255,255,0.8); 72 | box-shadow: 0 0 15px rgba(0,0,0,0.2); 73 | border-radius: 5px; 74 | } 75 | 76 | .icon-control{ 77 | background-repeat: no-repeat; 78 | background-size: auto auto; 79 | background-position: center; 80 | width: 40px; 81 | height: 40px; 82 | margin-top: 0; 83 | padding: 5px 5px; 84 | cursor: pointer; 85 | } 86 | 87 | .fit-control{ 88 | background-image: url("../images/fa-icons/resize-small.svg"); 89 | } 90 | 91 | .clear-control{ 92 | background-image: url("../images/fa-icons/trash.svg"); 93 | } 94 | 95 | .solve-control{ 96 | background-image: url("../images/fa-icons/cog-alt.svg"); 97 | } 98 | 99 | .collapse-control{ 100 | background-repeat: no-repeat; 101 | background-size: auto auto; 102 | background-position: center; 103 | width: 13px; 104 | height: 13px; 105 | margin-top: 0; 106 | padding: 8px 8px; 107 | cursor: pointer; 108 | } 109 | 110 | .collapse-control-right{ 111 | background-image: url("../images/fa-icons/chevron-right.svg"); 112 | } 113 | 114 | .collapse-control-left{ 115 | background-image: url("../images/fa-icons/chevron-left.svg"); 116 | } 117 | 118 | .summary-control{ 119 | padding: 0px 5px; 120 | font-size: 14px; 121 | } 122 | 123 | .job-options{ 124 | font-weight: bold; 125 | cursor: pointer; 126 | } 127 | 128 | .vehicle-title{ 129 | font-size: 18px; 130 | font-weight: bold; 131 | } 132 | 133 | .vehicle-start{ 134 | border-style: solid; 135 | border-width: 0px 0px 0px 6px; 136 | border-color: #2dbe21; 137 | } 138 | 139 | .vehicle-end{ 140 | border-style: solid; 141 | border-width: 0px 0px 0px 6px; 142 | border-color: #e9130a; 143 | } 144 | 145 | .wait-display{ 146 | text-align: center; 147 | } 148 | 149 | i.wait-icon{ 150 | margin: auto; 151 | display: block; 152 | background-repeat: no-repeat; 153 | background-size: contain; 154 | background-position: center; 155 | padding: 0px 5px 0px 5px; 156 | background-image: url("../images/throbber.gif"); 157 | width: 48px; 158 | height: 48px; 159 | } 160 | 161 | .rank{ 162 | font-weight: bold; 163 | } 164 | 165 | .solution-display{ 166 | border-style: solid; 167 | border-width: 2px; 168 | padding: 2px 5px; 169 | text-align: right; 170 | } 171 | 172 | .overpass-button { 173 | float: right; 174 | } 175 | 176 | .overpass-description { 177 | font-weight: bolder; 178 | } 179 | 180 | .overpass-table { 181 | margin-top: 4px; 182 | overflow: auto; 183 | text-align: left; 184 | display: table; 185 | max-width: 280px; 186 | min-width: 180px; 187 | width: 90%; 188 | } 189 | 190 | .overpass-tag { 191 | width: 40%; 192 | min-width: 60px; 193 | } 194 | .overpass-value { 195 | width: 40%; 196 | min-width: 60px; 197 | } 198 | 199 | .leaflet-control-geocoder{ 200 | clear: none; 201 | } 202 | 203 | #init-display{ 204 | font-size: 14px; 205 | border-style: solid; 206 | border-width: 2px; 207 | position: absolute; 208 | top: 35%; 209 | width: 80%; 210 | padding: 5px; 211 | } 212 | 213 | input[type='file'] { 214 | color: transparent; 215 | } 216 | -------------------------------------------------------------------------------- /images/fa-icons/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | chevron-left 4 | 5 | 6 | -------------------------------------------------------------------------------- /images/fa-icons/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | chevron-right 4 | 5 | 6 | -------------------------------------------------------------------------------- /images/fa-icons/cog-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /images/fa-icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 16 | 18 | image/svg+xml 19 | 21 | 22 | 23 | 24 | 25 | 27 | 30 | 31 | -------------------------------------------------------------------------------- /images/fa-icons/resize-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /images/fa-icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /images/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VROOM-Project/vroom-frontend/2f5484661685dd383e6c5a914f8527b14a8eb774/images/throbber.gif -------------------------------------------------------------------------------- /images/vroom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 15 | 16 | 17 | 18 | 19 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 36 | 37 | 38 | 39 | 40 | 46 | 50 | 53 | 59 | 60 | 66 | 74 | 82 | 87 | 90 | 93 | 98 | 105 | 112 | 119 | 125 | 129 | 130 | 137 | 145 | 150 | 154 | 159 | 165 | 172 | 179 | 182 | 185 | 192 | 195 | 197 | 204 | 207 | 210 | 218 | 222 | 224 | 231 | 237 | 241 | 244 | 248 | 254 | 255 | 256 | 257 | 261 | 270 | 279 | 287 | 288 | 289 | 290 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vehicle Routing Open-source Optimization Machine (DEMO) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vroom-frontend", 3 | "version": "0.3.0", 4 | "description": "Frontend demo for VROOM", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "serve": "./node_modules/.bin/beefy src/map.js --browserify ./node_modules/.bin/browserify --live", 8 | "build": "./node_modules/.bin/browserify -d src/map.js -o bundle.raw.js && ./node_modules/.bin/uglifyjs bundle.raw.js -c -m -o bundle.js", 9 | "dist": "./scripts/dist.sh" 10 | }, 11 | "keywords": [ 12 | "VROOM", 13 | "frontend", 14 | "optimization", 15 | "TSP", 16 | "VRP", 17 | "routing", 18 | "OSRM", 19 | "OSM" 20 | ], 21 | "author": "Julien Coupey", 22 | "license": "BSD-2-Clause", 23 | "devDependencies": { 24 | "beefy": "^2.1.8", 25 | "browserify": "^13.3.0", 26 | "uglify-js": "^2.8.29" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/VROOM-Project/vroom-frontend.git" 31 | }, 32 | "dependencies": { 33 | "@mapbox/polyline": "^0.2.0", 34 | "leaflet": "^1.3.4", 35 | "leaflet-control-geocoder": "^1.6.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /scripts/dist.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | npm run build 4 | rm bundle.raw.js 5 | 6 | rm -rf dist 7 | mkdir dist 8 | 9 | mv -v bundle.js dist 10 | cp -rv css dist 11 | cp -rv images dist 12 | cp -v index.html dist 13 | 14 | sed -i 's/src\/map.js/bundle.js/' dist/index.html 15 | 16 | cp -v ./node_modules/leaflet/dist/leaflet.css dist/css/leaflet.css 17 | 18 | cp -v ./node_modules/leaflet-control-geocoder/dist/Control.Geocoder.css dist/css/Control.Geocoder.css 19 | cp -rv ./node_modules/leaflet-control-geocoder/dist/images dist/css/ 20 | 21 | sed -i 's/node_modules\/leaflet\/dist/css/' dist/index.html 22 | sed -i 's/node_modules\/leaflet-control-geocoder\/dist/css/' dist/index.html 23 | 24 | sed -i 's/\.\.\/\.\.\/images\/vroom/\.\/images\/vroom/' dist/bundle.js 25 | 26 | -------------------------------------------------------------------------------- /src/config/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | tileLayer: 'http://{s}.tile.osm.org/{z}/{x}/{y}.png', 5 | host: 'http://localhost', 6 | port: '3000', 7 | maxTaskNumber: 100, 8 | overpassEndpoint: 'https://overpass-api.de/api/interpreter' 9 | }; 10 | -------------------------------------------------------------------------------- /src/config/leaflet_setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var L = require('leaflet'); 4 | var api = require('./api'); 5 | 6 | L.Icon.Default.imagePath = 'css/images/'; 7 | 8 | var initCenter = L.latLng(48.8579,2.3494); 9 | var initZoom = 13; 10 | 11 | var attribution = 'Routes computed using OSRM' 12 | + ' | ' 13 | + '© OpenStreetMap contributors'; 14 | if (api.description) { 15 | attribution = 'Demo solver hosted by ' 16 | + api.description 17 | + ' | ' + attribution; 18 | } 19 | 20 | var tileLayer = L.tileLayer(api.tileLayer, {attribution: attribution}); 21 | 22 | // Define a valid bounding box here in order to restrict map view and 23 | // place definition. 24 | var maxBounds = undefined; 25 | 26 | // Optional minZoom value. 27 | var minZoom = undefined; 28 | 29 | var map = L.map('map', {layers: [tileLayer]}) 30 | .setView(initCenter, initZoom) 31 | .setMaxBounds(maxBounds) 32 | .setMinZoom(minZoom); 33 | 34 | // Palette partly borrowed from https://clrs.cc/ 35 | var routeColors = [ 36 | '#0074D9', // blue 37 | '#FF851B', // orange 38 | '#B10DC9', // purple 39 | '#2ECC40', // green 40 | '#FFDC00', // yellow 41 | '#F012BE', // fuchsia 42 | '#01FF70', // lime 43 | '#999999', // gray 44 | '#001f3f', // navy 45 | '#FF4136', // red 46 | '#85144b', // maroon 47 | '#3D9970', // olive 48 | '#39CCCC', // teal 49 | ]; 50 | 51 | var markerStyle = { 52 | 'job': { 53 | 'color': '#3388ff', 54 | 'radius': 6 55 | }, 56 | 'pickup': { 57 | 'color': '#FF7900', 58 | 'radius': 8 59 | }, 60 | 'delivery': { 61 | 'color': '#FF7900', 62 | 'radius': 4 63 | }, 64 | 'unassigned': { 65 | 'color': '#111111', 66 | 'radius': 8 67 | } 68 | }; 69 | 70 | var pdLineStyle = { 71 | 'color': '#666666', 72 | 'weight': 4, 73 | 'opacity': 0.8 74 | } 75 | 76 | module.exports = { 77 | map: map, 78 | maxBounds: maxBounds, 79 | initCenter: initCenter, 80 | initZoom: initZoom, 81 | tileLayer: tileLayer, 82 | lowOpacity: 0.4, 83 | opacity: 0.6, 84 | highOpacity: 0.9, 85 | labelOpacity: 0.9, 86 | weight: 8, 87 | routeColors: routeColors, 88 | startColor: '#48b605', 89 | endColor: '#e9130a', 90 | markerStyle: markerStyle, 91 | pdLineStyle: pdLineStyle 92 | }; 93 | -------------------------------------------------------------------------------- /src/controls/clear.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var clearControl = L.Control.extend({ 4 | options: { 5 | position: 'topleft' 6 | }, 7 | 8 | onAdd: function (map) { 9 | // Add reference to map. 10 | map.clearControl = this; 11 | this._div = L.DomUtil.create('div', 'custom-control icon-control clear-control'); 12 | this._div.title = 'Clear'; 13 | 14 | this._div.onclick = function(e) { 15 | L.DomEvent.stopPropagation(e); 16 | map.fireEvent('clear'); 17 | }; 18 | return this._div; 19 | }, 20 | 21 | onRemove: function (map) { 22 | // Remove reference from map. 23 | delete map.clearControl; 24 | } 25 | }); 26 | 27 | var clearControl = new clearControl(); 28 | 29 | module.exports = clearControl; 30 | -------------------------------------------------------------------------------- /src/controls/collapse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var collapseControl = L.Control.extend({ 4 | options: { 5 | position: 'topright' 6 | }, 7 | 8 | onAdd: function (map) { 9 | // Add reference to map. 10 | map.collapseControl = this; 11 | this._div = L.DomUtil.create('div', 'custom-control icon-control collapse-control collapse-control-right'); 12 | this._div.title = 'Collapse'; 13 | 14 | this._div.onclick = function(e) { 15 | L.DomEvent.stopPropagation(e); 16 | map.fireEvent('collapse'); 17 | }; 18 | return this._div; 19 | }, 20 | 21 | onRemove: function (map) { 22 | // Remove reference from map. 23 | delete map.collapseControl; 24 | }, 25 | 26 | toggle: function() { 27 | var right = 'collapse-control-right'; 28 | var left = 'collapse-control-left'; 29 | if (this._div.classList.contains(right)) { 30 | this._div.classList.remove(right); 31 | this._div.classList.add(left); 32 | } else { 33 | this._div.classList.remove(left); 34 | this._div.classList.add(right); 35 | } 36 | } 37 | }); 38 | 39 | var collapseControl = new collapseControl(); 40 | 41 | module.exports = collapseControl; 42 | -------------------------------------------------------------------------------- /src/controls/fit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var L = require('leaflet'); 4 | 5 | var fitControl = L.Control.extend({ 6 | options: { 7 | position: 'topleft' 8 | }, 9 | 10 | onAdd: function (map) { 11 | // Add reference to map. 12 | map.fitControl = this; 13 | this._div = L.DomUtil.create('div', 'custom-control icon-control fit-control'); 14 | this._div.title = 'Show all places'; 15 | this._div.onclick = function(e) { 16 | L.DomEvent.stopPropagation(e); 17 | map.fireEvent('fit'); 18 | }; 19 | 20 | return this._div; 21 | }, 22 | 23 | onRemove: function (map) { 24 | // Remove reference from map. 25 | delete map.fitControl; 26 | } 27 | }); 28 | 29 | var fitControl = new fitControl(); 30 | 31 | module.exports = fitControl; 32 | -------------------------------------------------------------------------------- /src/controls/panel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var L = require('leaflet'); 4 | 5 | var panelControl = L.Control.extend({ 6 | options: { 7 | position: 'topright' 8 | }, 9 | 10 | onAdd: function (map) { 11 | // Add reference to map. 12 | map.panelControl = this; 13 | 14 | // Main panel div. 15 | this._div = L.DomUtil.create('div', 'panel-control'); 16 | 17 | // Header for panel control. 18 | var headerDiv = document.createElement('div'); 19 | headerDiv.setAttribute('class', 'panel-header'); 20 | headerDiv.innerHTML = 'Vroom'; 21 | this._div.appendChild(headerDiv); 22 | 23 | // Wait icon displayed while solving. 24 | this._waitDisplayDiv = document.createElement('div'); 25 | this._waitDisplayDiv.setAttribute('class', 'wait-display'); 26 | var waitIcon = document.createElement('i'); 27 | waitIcon.setAttribute('id', 'wait-icon'); 28 | this._waitDisplayDiv.appendChild(waitIcon); 29 | this._div.appendChild(this._waitDisplayDiv); 30 | 31 | // Initial displayed message. 32 | this._initDiv = document.createElement('div'); 33 | this._initDiv.setAttribute('id', 'init-display'); 34 | 35 | var header = document.createElement('p'); 36 | header.innerHTML = 'Add locations either by:' 37 | 38 | var list = document.createElement('ul'); 39 | var clickEl = document.createElement('li'); 40 | clickEl.innerHTML = 'clicking on the map;'; 41 | list.appendChild(clickEl); 42 | var uploadEl = document.createElement('li'); 43 | uploadEl.innerHTML = 'using a file with one address (or Lat,Lng coord) on each line.'; 44 | list.appendChild(uploadEl); 45 | 46 | var jsonUploadEl = document.createElement('li'); 47 | jsonUploadEl.innerHTML = 'using a json-formatted file.'; 48 | 49 | var fileInput = document.createElement('input'); 50 | fileInput.setAttribute('type', 'file'); 51 | fileInput.setAttribute('id', 'user-file'); 52 | 53 | jsonUploadEl.appendChild(fileInput); 54 | list.appendChild(jsonUploadEl); 55 | 56 | this._initDiv.appendChild(header); 57 | this._initDiv.appendChild(list); 58 | this._div.appendChild(this._initDiv); 59 | 60 | // Table for vehicles display. 61 | this._vehiclesDiv = document.createElement('div'); 62 | this._vehiclesDiv.setAttribute('id', 'panel-vehicles'); 63 | 64 | // Table for tasks display. 65 | this._taskTable = document.createElement('table'); 66 | this._taskTable.setAttribute('id', 'panel-tasks'); 67 | this._taskTable.setAttribute('class', 'panel-table'); 68 | 69 | // Table for task-ordered solution display. 70 | this._solutionTable = document.createElement('table'); 71 | this._solutionTable.setAttribute('id', 'panel-solution'); 72 | this._solutionTable.setAttribute('class', 'panel-table'); 73 | 74 | // Form for the Overpass query 75 | this._overpassDiv = document.createElement('div'); 76 | this._overpassDiv.setAttribute('id', 'panel-overpass'); 77 | this._overpassDiv.style.display = 'none'; 78 | 79 | this.addOverpassForm(map); 80 | 81 | var tableDiv = document.createElement('div'); 82 | 83 | tableDiv.appendChild(this._vehiclesDiv); 84 | tableDiv.appendChild(document.createElement('hr')); 85 | tableDiv.appendChild(this._overpassDiv); 86 | tableDiv.appendChild(this._taskTable); 87 | tableDiv.appendChild(this._solutionTable); 88 | this._div.appendChild(tableDiv); 89 | 90 | // Prevent events on this control to alter the underlying map. 91 | L.DomEvent.disableClickPropagation(this._div); 92 | L.DomEvent.on(this._div, 'mousewheel', L.DomEvent.stopPropagation); 93 | 94 | return this._div; 95 | }, 96 | 97 | onRemove: function(map) { 98 | // Remove reference from map. 99 | delete map.panelControl; 100 | }, 101 | 102 | clearTaskDisplay: function() { 103 | // Delete tasks display. 104 | for (var i = this._taskTable.rows.length; i > 0; i--) { 105 | this._taskTable.deleteRow(i -1); 106 | } 107 | }, 108 | 109 | clearVehiclesDisplay: function() { 110 | // Delete vehicles div. 111 | this._vehiclesDiv.innerHTML = ""; 112 | }, 113 | 114 | clearDisplay: function() { 115 | this.clearTaskDisplay(); 116 | this.clearVehiclesDisplay(); 117 | this.hideOverpassDisplay(); 118 | this.showInitDiv(); 119 | }, 120 | 121 | clearSolutionDisplay: function() { 122 | for (var i = this._solutionTable.rows.length; i > 0; i--) { 123 | this._solutionTable.deleteRow(i -1); 124 | } 125 | }, 126 | 127 | hideTaskDisplay: function() { 128 | this._taskTable.style.display = 'none'; 129 | }, 130 | 131 | showTaskDisplay: function() { 132 | this._taskTable.style.display = 'block'; 133 | }, 134 | 135 | hideInitDiv: function() { 136 | this._initDiv.style.display = 'none'; 137 | }, 138 | 139 | showInitDiv: function() { 140 | this._initDiv.style.display = 'block'; 141 | }, 142 | 143 | hideOverpassDisplay: function() { 144 | this._overpassDiv.style.display = 'none'; 145 | }, 146 | 147 | showOverpassDisplay: function() { 148 | this._overpassDiv.style.display = 'block'; 149 | }, 150 | 151 | hideOverpassButton: function() { 152 | document.getElementById('button-request').style.display = 'none'; 153 | }, 154 | 155 | showOverpassButton: function() { 156 | document.getElementById('button-request').style.display = 'block'; 157 | }, 158 | 159 | addOverpassForm: function(map) { 160 | var overpassForm = document.createElement('table'); 161 | overpassForm.setAttribute('id', 'table-overpass'); 162 | 163 | // Title 164 | var overpassHeading = document.createElement('h2'); 165 | overpassHeading.innerHTML = 'Add locations'; 166 | overpassForm.appendChild(overpassHeading); 167 | var clickOption = document.createElement('div'); 168 | clickOption.setAttribute('class', 'overpass-description'); 169 | clickOption.innerHTML = '- by clicking on the map'; 170 | overpassForm.appendChild(clickOption); 171 | 172 | // Table containing the Formular 173 | var tagTable = document.createElement('table'); 174 | tagTable.setAttribute('class', 'overpass-table'); 175 | 176 | // Subtitle 177 | var overpassSubtitle = document.createElement('text'); 178 | var tagsText = 'tag'; 179 | overpassSubtitle.innerHTML = '- using OpenStreetMap ' + tagsText.link('https://wiki.openstreetmap.org/wiki/Tags'); 180 | overpassSubtitle.setAttribute('class', 'overpass-description'); 181 | tagTable.appendChild(overpassSubtitle); 182 | 183 | var newLine = document.createElement ("br"); 184 | tagTable.appendChild(newLine); 185 | 186 | // Formular cells 187 | var lineForm = document.createElement('form-inline'); 188 | lineForm.setAttribute('id', 'tag-table'); 189 | lineForm.setAttribute('class', 'overpass-tag-table'); 190 | 191 | // Key cell 192 | var keyelement = document.createElement('input'); 193 | keyelement.setAttribute('id', 'key-cell'); 194 | keyelement.setAttribute('class', 'overpass-tag'); 195 | keyelement.setAttribute('type', 'texte'); 196 | keyelement.setAttribute('value', 'amenity'); 197 | lineForm.appendChild(keyelement); 198 | 199 | // Value cell 200 | var valueelement = document.createElement('input'); 201 | valueelement.setAttribute('id', 'value-cell'); 202 | valueelement.setAttribute('class', 'overpass-value'); 203 | valueelement.setAttribute('type', 'texte'); 204 | valueelement.setAttribute('value', 'pharmacy'); 205 | lineForm.appendChild(valueelement); 206 | 207 | tagTable.appendChild(lineForm); 208 | 209 | // Description 210 | var overpassDescription = document.createElement('text'); 211 | var amenity_text = 'amenity' 212 | overpassDescription.innerHTML = 'More values for ' + amenity_text.link('https://wiki.openstreetmap.org/wiki/Key:amenity') + '.'; 213 | tagTable.appendChild(overpassDescription); 214 | 215 | var newLine = document.createElement ("br"); 216 | tagTable.appendChild(newLine); 217 | 218 | // Submit button 219 | var submitelement = document.createElement('input'); 220 | submitelement.setAttribute('id', 'button-request'); 221 | submitelement.setAttribute('class', 'overpass-button'); 222 | submitelement.setAttribute('type', 'button'); 223 | submitelement.setAttribute('value', 'Add'); 224 | 225 | // Call overpass 226 | submitelement.onclick = function(e) { 227 | if (map.getZoom() < 9) { 228 | alert("The area is too large, please zoom in."); 229 | return; 230 | } 231 | L.DomEvent.stopPropagation(e); 232 | document.getElementById('wait-icon').setAttribute('class', 'wait-icon'); 233 | panelControl.hideOverpassButton(); 234 | map.fireEvent('overpass'); 235 | }; 236 | 237 | tagTable.appendChild(submitelement); 238 | overpassForm.appendChild(tagTable); 239 | this._overpassDiv.appendChild(overpassForm); 240 | }, 241 | 242 | toggle: function() { 243 | if (this._div.style.visibility == 'hidden') { 244 | this._div.style.visibility = 'visible'; 245 | } else { 246 | this._div.style.visibility = 'hidden'; 247 | } 248 | }, 249 | 250 | getWidth: function() { 251 | var width = this._div.offsetWidth; 252 | if (this._div.style.visibility == 'hidden') { 253 | width = 0; 254 | } 255 | return width; 256 | } 257 | }); 258 | 259 | var panelControl = new panelControl(); 260 | 261 | module.exports = panelControl; 262 | -------------------------------------------------------------------------------- /src/controls/solve.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var solveControl = L.Control.extend({ 4 | options: { 5 | position: 'topleft' 6 | }, 7 | 8 | onAdd: function (map) { 9 | // Add reference to map. 10 | map.solveControl = this; 11 | this._div = L.DomUtil.create('div', 'custom-control icon-control solve-control'); 12 | this._div.title = 'Solve'; 13 | this._div.onclick = function(e) { 14 | L.DomEvent.stopPropagation(e); 15 | map.removeControl(solveControl); 16 | document.getElementById('wait-icon').setAttribute('class', 'wait-icon'); 17 | 18 | map.fireEvent('solve'); 19 | }; 20 | return this._div; 21 | }, 22 | 23 | onRemove: function (map) { 24 | // Remove reference from map. 25 | delete map.solveControl; 26 | } 27 | }); 28 | 29 | var solveControl = new solveControl(); 30 | 31 | module.exports = solveControl; 32 | -------------------------------------------------------------------------------- /src/controls/summary.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var time = require('../utils/timing'); 4 | 5 | var summaryControl = L.Control.extend({ 6 | options: { 7 | position: 'bottomleft' 8 | }, 9 | 10 | onAdd: function (map) { 11 | // Add reference to map. 12 | map.summaryControl = this; 13 | this._div = L.DomUtil.create('div', 'custom-control summary-control'); 14 | return this._div; 15 | }, 16 | 17 | onRemove: function (map) { 18 | // Remove reference from map. 19 | delete map.summaryControl; 20 | }, 21 | 22 | update: function(solution) { 23 | this._div.innerHTML = ''; 24 | 25 | var displayDuration = document.createElement('p'); 26 | displayDuration.innerHTML = 'Trip duration: ' 27 | + time.format(solution['summary']['duration']); 28 | this._div.appendChild(displayDuration); 29 | 30 | var displayDistance = document.createElement('p'); 31 | var distance = (solution['summary']['distance'] / 1000).toFixed(1); 32 | displayDistance.innerHTML = 'Trip distance: ' 33 | + distance.toString() + ' km'; 34 | this._div.appendChild(displayDistance); 35 | 36 | // Computing time stuff. 37 | var CTLoading = solution['summary']['computing_times']['loading']; 38 | var CTSolving = solution['summary']['computing_times']['solving']; 39 | var CTRouting = solution['summary']['computing_times']['routing']; 40 | 41 | var CTDisplay = document.createElement('p'); 42 | CTDisplay.title = 'Loading: ' + CTLoading + ' ms / Solving: ' 43 | + CTSolving + ' ms/ Routing: ' + CTRouting + ' ms'; 44 | 45 | var ct = CTLoading + CTSolving + CTRouting; 46 | CTDisplay.innerHTML = 'Computing time: ' + (ct / 1000) + ' s'; 47 | this._div.appendChild(CTDisplay); 48 | } 49 | }); 50 | 51 | var summaryControl = new summaryControl(); 52 | 53 | module.exports = summaryControl; 54 | -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var jobs = []; 4 | var shipments = []; 5 | var vehicles = []; 6 | 7 | // Stored with type and task id as key, e.g. markers['job']['14']. 8 | var markers = { 9 | 'job': {}, 10 | 'pickup': {}, 11 | 'delivery': {} 12 | }; 13 | 14 | // Stored with vehicle id + {_start,_end} as key 15 | var vehiclesMarkers = {}; 16 | 17 | var maxTaskId = 0; 18 | var maxVehicleId = 0; 19 | 20 | // Store segments to match pickup and delivery, indexed in the form 21 | // 'p.id-d.id'. 22 | var pdLines = {}; 23 | 24 | module.exports = { 25 | jobs: jobs, 26 | shipments: shipments, 27 | maxTaskId: maxTaskId, 28 | maxVehicleId: maxVehicleId, 29 | vehicles: vehicles, 30 | markers: markers, 31 | vehiclesMarkers: vehiclesMarkers, 32 | pdLines: pdLines 33 | }; 34 | -------------------------------------------------------------------------------- /src/map.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LSetup = require('./config/leaflet_setup'); 4 | var panelControl = require('./controls/panel'); 5 | var collapseControl = require('./controls/collapse'); 6 | var locationsHandler = require('./utils/locations'); 7 | var geocoder = require('./utils/geocoder'); 8 | var overpass = require('./utils/overpass'); 9 | var address = require('./utils/address'); 10 | var fileHandler = require('./utils/file_handler'); 11 | var solutionHandler = require('./utils/solution_handler'); 12 | 13 | panelControl.addTo(LSetup.map); 14 | collapseControl.addTo(LSetup.map); 15 | fileHandler.setFile(); 16 | 17 | LSetup.map.on('click', function(e) { 18 | locationsHandler.addPlace(e.latlng); 19 | }); 20 | 21 | LSetup.map.on('solve', solutionHandler.solve); 22 | 23 | LSetup.map.on('overpass', overpass.query); 24 | 25 | geocoder.control.markGeocode = function(result) { 26 | locationsHandler.addPlace(result.geocode.center, 27 | address.display(result.geocode)); 28 | }; 29 | 30 | geocoder.control.addTo(LSetup.map); 31 | -------------------------------------------------------------------------------- /src/utils/address.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Use the result of a reverse geocoding query to compute a simplified 4 | // string describing the address. 5 | var display = function(reverseGeo) { 6 | var address = reverseGeo.properties.address 7 | var name = ''; 8 | if (address.house_number) { 9 | name += address.house_number; 10 | } 11 | if (address.road) { 12 | if (name.length !== 0) { 13 | name += ', '; 14 | } 15 | name += address.road; 16 | } 17 | if (!address.road && address.pedestrian) { 18 | if (name.length !== 0) { 19 | name += ', ' 20 | } 21 | name += address.pedestrian; 22 | } 23 | if (!address.road && !address.pedestrian && address.suburb) { 24 | if (name.length !== 0) { 25 | name += ', ' 26 | } 27 | name += address.suburb; 28 | } 29 | if (address.village) { 30 | if (name.length != 0) { 31 | name += ', '; 32 | } 33 | name += address.village; 34 | } 35 | if (address.town) { 36 | if (name.length != 0) { 37 | name += ', '; 38 | } 39 | name += address.town; 40 | } 41 | if (address.city) { 42 | if (name.length != 0) { 43 | name += ', '; 44 | } 45 | name += address.city; 46 | } 47 | if (name.length === 0 && address.country) { 48 | name = address.country; 49 | } 50 | return name; 51 | } 52 | 53 | module.exports = { 54 | display: display 55 | }; 56 | -------------------------------------------------------------------------------- /src/utils/data_handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LSetup = require('../config/leaflet_setup'); 4 | var api = require('../config/api'); 5 | 6 | var polyUtil = require('@mapbox/polyline'); 7 | var data = require('../data'); 8 | var panelControl = require('../controls/panel'); 9 | var collapseControl = require('../controls/collapse'); 10 | var fitControl = require('../controls/fit'); 11 | var clearControl = require('../controls/clear'); 12 | var solveControl = require('../controls/solve'); 13 | var summaryControl = require('../controls/summary'); 14 | 15 | var routes = []; 16 | var actualSteps = ['job', 'pickup', 'delivery']; 17 | 18 | var getJobs = function() { 19 | return data.jobs; 20 | } 21 | 22 | var getShipments = function() { 23 | return data.shipments; 24 | } 25 | 26 | var getVehicles = function() { 27 | return data.vehicles; 28 | } 29 | 30 | var getOverpassQuery = function() { 31 | var key = document.getElementById('key-cell').value; 32 | var value = document.getElementById('value-cell').value; 33 | var bounds = LSetup.map.getBounds(); 34 | var query = '[out:json];node(' + bounds.getSouth() + ',' + 35 | bounds.getWest() + ',' + bounds.getNorth() + ','+ 36 | bounds.getEast() + ')[' + key + '=' + value + '];out;' 37 | return query; 38 | } 39 | 40 | var _getTasksSize = function() { 41 | return data.jobs.length + 2 * data.shipments.length; 42 | } 43 | 44 | var getNextTaskId = function() { 45 | return data.maxTaskId + 1; 46 | } 47 | 48 | var getNextVehicleId = function() { 49 | return data.maxVehicleId + 1; 50 | } 51 | 52 | var _getVehiclesSize = function() { 53 | return data.vehicles.length; 54 | } 55 | 56 | var checkControls = function() { 57 | var hasTasks = _getTasksSize() > 0; 58 | var hasVehicles = _getVehiclesSize() > 0; 59 | if (hasTasks || hasVehicles) { 60 | // Fit and clear controls as soon as we have a location. 61 | if (!LSetup.map.fitControl) { 62 | LSetup.map.addControl(fitControl); 63 | } 64 | if (!LSetup.map.clearControl) { 65 | LSetup.map.addControl(clearControl); 66 | } 67 | } 68 | if (!LSetup.map.solveControl) { 69 | // Solve control appears only when there's enough input to fire a 70 | // solving query. 71 | if (hasVehicles && hasTasks) { 72 | solveControl.addTo(LSetup.map); 73 | } 74 | } else { 75 | if (_getTasksSize() === 0) { 76 | LSetup.map.removeControl(solveControl); 77 | } 78 | } 79 | if (hasSolution()) { 80 | LSetup.map.removeControl(solveControl); 81 | LSetup.map.addControl(summaryControl); 82 | } 83 | panelControl.showOverpassButton(); 84 | } 85 | 86 | var _pushToBounds = function(coords) { 87 | if (data.bounds) { 88 | data.bounds.extend([coords[1], coords[0]]); 89 | } else { 90 | data.bounds = L.latLngBounds([coords[1], coords[0]], 91 | [coords[1], coords[0]]); 92 | } 93 | } 94 | 95 | var _recomputeBounds = function() { 96 | // Problem bounds are extended upon additions but they need to be 97 | // recalculated when a deletion might reduce the bounds. 98 | delete data.bounds; 99 | 100 | for (var i = 0; i < data.vehicles.length; i++) { 101 | var start = data.vehicles[i].start; 102 | if (start) { 103 | _pushToBounds(start); 104 | } 105 | var end = data.vehicles[i].end; 106 | if (end) { 107 | _pushToBounds(end); 108 | } 109 | } 110 | 111 | for (var i = 0; i < data.jobs.length; i++) { 112 | var loc = data.jobs[i].location; 113 | _pushToBounds(loc); 114 | } 115 | } 116 | 117 | var fitView = function() { 118 | if (data.bounds) { 119 | LSetup.map.fitBounds(data.bounds, { 120 | paddingBottomRight: [panelControl.getWidth(), 0], 121 | paddingTopLeft: [50, 0], 122 | }); 123 | } 124 | } 125 | 126 | var hasSolution = function() { 127 | return routes.length > 0; 128 | } 129 | 130 | // Used upon addition to distinguish between start/end or job 131 | // addition. 132 | var _firstPlace = true; 133 | 134 | var isFirstPlace = function() { 135 | return _firstPlace; 136 | } 137 | 138 | var firstPlaceSet = function() { 139 | _firstPlace = false; 140 | } 141 | 142 | var _hasCapacity = true; 143 | 144 | var hasCapacity = function() { 145 | return _hasCapacity; 146 | } 147 | 148 | var _clearSolution = function() { 149 | if (hasSolution()) { 150 | // Back to input mode. 151 | panelControl.clearSolutionDisplay(); 152 | panelControl.showTaskDisplay(); 153 | 154 | for (var i = 0; i < routes.length; ++i) { 155 | LSetup.map.removeLayer(routes[i]); 156 | } 157 | for (var type in data.markers) { 158 | for (var k in data.markers[type]) { 159 | data.markers[type][k].setStyle(LSetup.markerStyle[type]); 160 | } 161 | } 162 | LSetup.map.removeControl(summaryControl); 163 | 164 | routes = []; 165 | 166 | // Remove query solution. 167 | delete data.solution; 168 | } 169 | } 170 | 171 | var clearData = function() { 172 | // Back to adding a start/end for next place. 173 | _firstPlace = true; 174 | _hasCapacity = true; 175 | data.maxTaskId = 0; 176 | data.maxVehicleId = 0; 177 | 178 | // Clear all data and markers. 179 | for (var type in data.markers) { 180 | for (var k in data.markers[type]) { 181 | LSetup.map.removeLayer(data.markers[type][k]); 182 | delete data.markers[type][k]; 183 | } 184 | } 185 | for (var k in data.vehiclesMarkers) { 186 | LSetup.map.removeLayer(data.vehiclesMarkers[k]); 187 | delete data.vehiclesMarkers[k]; 188 | } 189 | 190 | // Init dataset. 191 | data.jobs = []; 192 | data.shipments = []; 193 | data.vehicles = []; 194 | data.markers = { 195 | 'job': {}, 196 | 'pickup': {}, 197 | 'delivery': {} 198 | }; 199 | data.vehiclesMarkers = {}; 200 | data.pdLines = {}; 201 | 202 | // Reset bounds. 203 | delete data.bounds; 204 | 205 | _clearSolution(); 206 | } 207 | 208 | var closeAllPopups = function() { 209 | for (var type in data.markers) { 210 | for (var k in data.markers[type]) { 211 | data.markers[type][k].closePopup(); 212 | } 213 | } 214 | for (var k in data.vehiclesMarkers) { 215 | data.vehiclesMarkers[k].closePopup(); 216 | } 217 | } 218 | 219 | var _setStart = function(v) { 220 | var vTable = document.getElementById('panel-vehicles-' + v.id.toString()); 221 | 222 | vTable.deleteRow(1); 223 | var row = vTable.insertRow(1); 224 | row.setAttribute('class', 'panel-table'); 225 | var idCell = row.insertCell(0); 226 | 227 | var remove = function() { 228 | if (_removeStart(v)) { 229 | // Reset start row when removing is ok. 230 | vTable.deleteRow(1); 231 | vTable.insertRow(1); 232 | if (_getTasksSize() === 0 && _getVehiclesSize() === 0) { 233 | LSetup.map.removeControl(clearControl); 234 | } 235 | checkControls(); 236 | } 237 | } 238 | idCell.setAttribute('class', 'delete-location'); 239 | idCell.title = 'Click to delete'; 240 | idCell.onclick = remove; 241 | 242 | // Required when parsing json files with no start description. 243 | if (!v.startDescription) { 244 | v.startDescription = 'Start'; 245 | } 246 | 247 | var nameCell = row.insertCell(1); 248 | nameCell.title = 'Click to center the map'; 249 | nameCell.setAttribute('class', 'vehicle-start'); 250 | nameCell.appendChild(document.createTextNode(v.startDescription)); 251 | nameCell.onclick = function() { 252 | showStart(v, true); 253 | }; 254 | 255 | // Marker and popup. 256 | data.vehiclesMarkers[v.id.toString() + '_start'] 257 | = L.circleMarker([v.start[1], v.start[0]], 258 | { 259 | radius: 8, 260 | weight: 3, 261 | fillOpacity: 0.4, 262 | color: LSetup.startColor 263 | }) 264 | .addTo(LSetup.map); 265 | 266 | var popupDiv = document.createElement('div'); 267 | var par = document.createElement('p'); 268 | par.innerHTML = 'Vehicle ' + v.id.toString() + ': ' + v.startDescription; 269 | var deleteButton = document.createElement('button'); 270 | deleteButton.innerHTML = 'Del'; 271 | deleteButton.onclick = remove; 272 | popupDiv.appendChild(par); 273 | popupDiv.appendChild(deleteButton); 274 | 275 | data.vehiclesMarkers[v.id.toString() + '_start'] 276 | .bindPopup(popupDiv) 277 | .openPopup(); 278 | } 279 | 280 | var _setEnd = function(v) { 281 | var vTable = document.getElementById('panel-vehicles-' + v.id.toString()); 282 | 283 | vTable.deleteRow(2); 284 | var row = vTable.insertRow(2); 285 | row.setAttribute('class', 'panel-table'); 286 | var idCell = row.insertCell(0); 287 | 288 | var remove = function() { 289 | if (_removeEnd(v)) { 290 | // Reset end row when removing is ok. 291 | vTable.deleteRow(2); 292 | vTable.insertRow(2); 293 | if (_getTasksSize() === 0 && _getVehiclesSize() === 0) { 294 | LSetup.map.removeControl(clearControl); 295 | } 296 | checkControls(); 297 | } 298 | } 299 | idCell.setAttribute('class', 'delete-location'); 300 | idCell.title = 'Click to delete'; 301 | idCell.onclick = remove; 302 | 303 | // Required when parsing json files with no end description. 304 | if (!v.endDescription) { 305 | v.endDescription = 'End'; 306 | } 307 | 308 | var nameCell = row.insertCell(1); 309 | nameCell.title = 'Click to center the map'; 310 | nameCell.setAttribute('class', 'vehicle-end'); 311 | nameCell.appendChild(document.createTextNode(v.endDescription)); 312 | nameCell.onclick = function() { 313 | showEnd(v, true); 314 | }; 315 | 316 | // Marker and popup. 317 | data.vehiclesMarkers[v.id.toString() + '_end'] 318 | = L.circleMarker([v.end[1], v.end[0]], 319 | { 320 | radius: 8, 321 | weight: 3, 322 | fillOpacity: 0.4, 323 | color: LSetup.endColor 324 | }) 325 | .addTo(LSetup.map); 326 | 327 | var popupDiv = document.createElement('div'); 328 | var par = document.createElement('p'); 329 | par.innerHTML = 'Vehicle ' + v.id.toString() + ': ' + v.endDescription; 330 | var deleteButton = document.createElement('button'); 331 | deleteButton.innerHTML = 'Del'; 332 | deleteButton.onclick = remove; 333 | popupDiv.appendChild(par); 334 | popupDiv.appendChild(deleteButton); 335 | 336 | data.vehiclesMarkers[v.id.toString() + '_end'] 337 | .bindPopup(popupDiv) 338 | .openPopup(); 339 | } 340 | 341 | var _deleteAmounts = function() { 342 | for (var v = 0; v < data.vehicles.length; v++) { 343 | delete data.vehicles[v].capacity; 344 | } 345 | for (var j = 0; j < data.jobs.length; j++) { 346 | delete data.jobs[j].delivery; 347 | delete data.jobs[j].pickup; 348 | } 349 | alert("All capacity constraints have been removed.") 350 | } 351 | 352 | var addVehicle = function(v) { 353 | _clearSolution(); 354 | data.vehicles.push(v); 355 | 356 | data.maxVehicleId = Math.max(data.maxVehicleId, v.id); 357 | 358 | var tableId = 'panel-vehicles-' + v.id.toString(); 359 | var vTable = document.getElementById(tableId); 360 | if (!vTable) { 361 | // Create new table for current vehicle. 362 | vTable = document.createElement('table'); 363 | vTable.setAttribute('id', tableId); 364 | vTable.setAttribute('class', 'panel-vehicle'); 365 | 366 | // Set title. 367 | var row = vTable.insertRow(0); 368 | 369 | var titleCell = row.insertCell(0); 370 | titleCell.setAttribute('colspan', 2); 371 | 372 | var titleName = document.createElement('span'); 373 | titleName.setAttribute('class', 'vehicle-title'); 374 | titleName.appendChild(document.createTextNode('Vehicle ' + v.id.toString())); 375 | 376 | var clone = document.createElement('span'); 377 | clone.setAttribute('class', 'clone-vehicle'); 378 | clone.appendChild(document.createTextNode('Clone >>')); 379 | clone.onclick = function() { 380 | var v_copy = JSON.parse(JSON.stringify(v)); 381 | v_copy.id = getNextVehicleId(); 382 | addVehicle(v_copy); 383 | checkControls(); 384 | }; 385 | 386 | titleCell.appendChild(titleName); 387 | titleCell.appendChild(clone); 388 | 389 | vTable.insertRow(1); 390 | vTable.insertRow(2); 391 | document.getElementById('panel-vehicles').appendChild(vTable); 392 | } 393 | 394 | if (v.start) { 395 | _pushToBounds(v.start); 396 | _setStart(v); 397 | } 398 | if (v.end) { 399 | _pushToBounds(v.end); 400 | _setEnd(v); 401 | } 402 | 403 | if (_hasCapacity && !('capacity' in v)) { 404 | _hasCapacity = false; 405 | if (_getVehiclesSize() + _getTasksSize() > 1) { 406 | _deleteAmounts(); 407 | } 408 | } 409 | 410 | _updateAllJobPopups(); 411 | } 412 | 413 | var _jobDisplay = function(j) { 414 | var panelList = document.getElementById('panel-tasks'); 415 | 416 | var nb_rows = panelList.rows.length; 417 | var row = panelList.insertRow(nb_rows); 418 | row.setAttribute('id', 'job-' + j.id.toString()); 419 | var idCell = row.insertCell(0); 420 | 421 | idCell.setAttribute('class', 'delete-location'); 422 | idCell.title = 'Click to delete'; 423 | idCell.onclick = function() { 424 | _removeJob(j); 425 | }; 426 | 427 | // Required when parsing json files containing jobs with no 428 | // description. 429 | if (!j.description) { 430 | j.description = 'No description'; 431 | } 432 | 433 | var nameCell = row.insertCell(1); 434 | nameCell.title = 'Click to center the map'; 435 | nameCell.appendChild(document.createTextNode(j.description)); 436 | nameCell.onclick = function() { 437 | _openPopup('job', j.id); 438 | centerMarker('job', j.id); 439 | }; 440 | 441 | _handleJobPopup(j); 442 | _openPopup('job', j.id); 443 | } 444 | 445 | var _setPanelTask = function(s, type) { 446 | var panelList = document.getElementById('panel-tasks'); 447 | 448 | var nb_rows = panelList.rows.length; 449 | var row = panelList.insertRow(nb_rows); 450 | row.setAttribute('id', type + '-' + s[type].id.toString()); 451 | var idCell = row.insertCell(0); 452 | 453 | idCell.setAttribute('class', 'delete-location'); 454 | idCell.title = 'Click to delete'; 455 | idCell.onclick = function() { 456 | _removeShipment(s); 457 | }; 458 | 459 | // Required when parsing json files containing jobs with no 460 | // description. 461 | if (!s[type].description) { 462 | s[type].description = 'No description'; 463 | } 464 | 465 | var nameCell = row.insertCell(1); 466 | nameCell.title = 'Click to center the map'; 467 | nameCell.appendChild(document.createTextNode(s[type].description)); 468 | nameCell.onclick = function() { 469 | _openPopup(type, s[type].id); 470 | centerMarker(type, s[type].id); 471 | }; 472 | } 473 | 474 | var _shipmentDisplay = function(s) { 475 | _setPanelTask(s, 'pickup'); 476 | _setPanelTask(s, 'delivery'); 477 | 478 | _handleShipmentPopup(s); 479 | } 480 | 481 | var _setAsStart = function(vRank, j) { 482 | var marker = data.vehicles[vRank].id.toString() + '_start'; 483 | LSetup.map.removeLayer(data.vehiclesMarkers[marker]); 484 | delete data.vehiclesMarkers[marker]; 485 | 486 | data.vehicles[vRank].start = j.location; 487 | data.vehicles[vRank].startDescription = j.description; 488 | _setStart(data.vehicles[vRank]); 489 | 490 | _removeJob(j); 491 | }; 492 | 493 | var _setAsEnd = function(vRank, j) { 494 | var marker = data.vehicles[vRank].id.toString() + '_end'; 495 | LSetup.map.removeLayer(data.vehiclesMarkers[marker]); 496 | delete data.vehiclesMarkers[marker]; 497 | 498 | data.vehicles[vRank].end = j.location; 499 | data.vehicles[vRank].endDescription = j.description; 500 | _setEnd(data.vehicles[vRank]); 501 | 502 | _removeJob(j); 503 | }; 504 | 505 | var _handleJobPopup = function(j) { 506 | var popupDiv = document.createElement('div'); 507 | var par = document.createElement('p'); 508 | par.innerHTML = 'Job ' + j.id + '
' + j.description; 509 | var deleteButton = document.createElement('button'); 510 | deleteButton.innerHTML = 'Del'; 511 | deleteButton.onclick = function() { 512 | _removeJob(j); 513 | }; 514 | 515 | var startSelect = document.createElement('select'); 516 | var startHeadOption = document.createElement('option'); 517 | startHeadOption.innerHTML = "Start"; 518 | startHeadOption.selected = true; 519 | startHeadOption.disabled = true; 520 | startSelect.appendChild(startHeadOption); 521 | 522 | var endSelect = document.createElement('select'); 523 | var endHeadOption = document.createElement('option'); 524 | endHeadOption.innerHTML = "End"; 525 | endHeadOption.selected = true; 526 | endHeadOption.disabled = true; 527 | endSelect.appendChild(endHeadOption); 528 | 529 | for (var v = 0; v < data.vehicles.length; v++) { 530 | var startOption = document.createElement('option'); 531 | startOption.value = v; 532 | startOption.innerHTML = 'v. ' + data.vehicles[v].id.toString(); 533 | startSelect.appendChild(startOption); 534 | 535 | var endOption = document.createElement('option'); 536 | endOption.value = v; 537 | endOption.innerHTML = 'v. ' + data.vehicles[v].id.toString(); 538 | endSelect.appendChild(endOption); 539 | } 540 | startSelect.onchange = function() { 541 | _setAsStart(startSelect.options[startSelect.selectedIndex].value, j); 542 | } 543 | endSelect.onchange = function() { 544 | _setAsEnd(endSelect.options[endSelect.selectedIndex].value, j); 545 | } 546 | 547 | var optionsDiv = document.createElement('div'); 548 | optionsDiv.appendChild(startSelect); 549 | optionsDiv.appendChild(endSelect); 550 | optionsDiv.appendChild(deleteButton); 551 | 552 | var optionsTitle = document.createElement('div'); 553 | optionsTitle.setAttribute('class', 'job-options'); 554 | optionsTitle.innerHTML = 'Options >>'; 555 | optionsTitle.onclick = function() { 556 | if (optionsDiv.style.display === 'none') { 557 | optionsDiv.style.display = 'flex'; 558 | } else { 559 | optionsDiv.style.display = 'none'; 560 | } 561 | _openPopup('job', j.id); 562 | } 563 | 564 | popupDiv.appendChild(par); 565 | popupDiv.appendChild(optionsTitle); 566 | popupDiv.appendChild(optionsDiv); 567 | optionsDiv.style.display = 'none'; 568 | 569 | data.markers['job'][j.id.toString()].bindPopup(popupDiv); 570 | 571 | data.markers['job'][j.id.toString()].on('popupclose', function() { 572 | optionsDiv.style.display = 'none'; 573 | }); 574 | } 575 | 576 | var _handleShipmentPopup = function(s) { 577 | data.pdLines[s.pickup.id + '-' + s.delivery.id] 578 | = L.polyline([[s.pickup.location[1], s.pickup.location[0]], 579 | [s.delivery.location[1], s.delivery.location[0]]], 580 | LSetup.pdLineStyle); 581 | 582 | for (var type of ['pickup', 'delivery']) { 583 | var popupDiv = document.createElement('div'); 584 | var par = document.createElement('p'); 585 | par.innerHTML = '' + (type.substring(0, 1)).toUpperCase() + 586 | type.substring(1, type.length) + ' ' + 587 | s[type].id + '
' + s[type].description; 588 | 589 | var deleteButton = document.createElement('button'); 590 | deleteButton.innerHTML = 'Del'; 591 | deleteButton.onclick = function() { 592 | _removeShipment(s); 593 | }; 594 | 595 | popupDiv.appendChild(par); 596 | popupDiv.appendChild(deleteButton); 597 | 598 | data.markers[type][s[type].id.toString()].bindPopup(popupDiv); 599 | 600 | data.markers[type][s[type].id.toString()].on('popupopen', function() { 601 | data.pdLines[s.pickup.id + '-' + s.delivery.id].addTo(LSetup.map); 602 | }); 603 | data.markers[type][s[type].id.toString()].on('mouseover', function() { 604 | data.pdLines[s.pickup.id + '-' + s.delivery.id].addTo(LSetup.map); 605 | }); 606 | data.markers[type][s[type].id.toString()].on('popupclose', function() { 607 | LSetup.map.removeLayer(data.pdLines[s.pickup.id + '-' + s.delivery.id]); 608 | }); 609 | data.markers[type][s[type].id.toString()].on('mouseout', function() { 610 | LSetup.map.removeLayer(data.pdLines[s.pickup.id + '-' + s.delivery.id]); 611 | }); 612 | } 613 | } 614 | 615 | var _openPopup = function(type, id) { 616 | data.markers[type][id.toString()].openPopup(); 617 | } 618 | 619 | var _updateAllJobPopups = function() { 620 | for (var i = 0; i < data.jobs.length; i++) { 621 | _handleJobPopup(data.jobs[i]); 622 | } 623 | } 624 | 625 | var centerMarker = function(type, id) { 626 | LSetup.map.panTo(data.markers[type][id.toString()].getLatLng()); 627 | } 628 | 629 | var addJob = function(j) { 630 | if (_getTasksSize() >= api.maxTaskNumber) { 631 | alert('Number of tasks can\'t exceed ' + api.maxTaskNumber + '.'); 632 | return; 633 | } 634 | 635 | if (_hasCapacity && !('delivery' in j) && !('pickup' in j)) { 636 | _hasCapacity = false; 637 | if (_getVehiclesSize() + _getTasksSize() > 1) { 638 | _deleteAmounts(); 639 | } 640 | } 641 | 642 | _clearSolution(); 643 | _pushToBounds(j.location); 644 | 645 | data.maxTaskId = Math.max(data.maxTaskId, j.id); 646 | data.jobs.push(j); 647 | data.markers['job'][j.id.toString()] 648 | = L.circleMarker([j.location[1], j.location[0]], 649 | { 650 | radius: LSetup.markerStyle['job'].radius, 651 | weight: 3, 652 | fillOpacity: 0.4, 653 | color: LSetup.markerStyle['job'].color 654 | }) 655 | .addTo(LSetup.map); 656 | 657 | // Handle display stuff. 658 | _jobDisplay(j); 659 | } 660 | 661 | var addShipment = function(s) { 662 | if (_getTasksSize() >= api.maxTaskNumber) { 663 | alert('Number of tasks can\'t exceed ' + api.maxTaskNumber + '.'); 664 | return; 665 | } 666 | 667 | if (_hasCapacity && !('amount' in s)) { 668 | _hasCapacity = false; 669 | if (_getVehiclesSize() + _getTasksSize() > 1) { 670 | _deleteAmounts(); 671 | } 672 | } 673 | 674 | _clearSolution(); 675 | 676 | for (var type of ['pickup', 'delivery']) { 677 | _pushToBounds(s[type].location); 678 | 679 | data.maxTaskId = Math.max(data.maxTaskId, s.pickup.id); 680 | data.maxTaskId = Math.max(data.maxTaskId, s.delivery.id); 681 | data.markers[type][s[type].id.toString()] 682 | = L.circleMarker([s[type].location[1], s[type].location[0]], 683 | { 684 | radius: LSetup.markerStyle[type].radius, 685 | weight: 3, 686 | fillOpacity: 0.4, 687 | color: LSetup.markerStyle[type].color 688 | }) 689 | .addTo(LSetup.map); 690 | } 691 | data.shipments.push(s); 692 | 693 | // Handle display stuff. 694 | _shipmentDisplay(s); 695 | } 696 | 697 | var _removeJob = function(j) { 698 | _clearSolution(); 699 | LSetup.map.removeLayer(data.markers['job'][j.id.toString()]); 700 | delete data.markers['job'][j.id.toString()]; 701 | for (var i = 0; i < data.jobs.length; i++) { 702 | if (data.jobs[i].id == j.id) { 703 | data.jobs.splice(i, 1); 704 | var jobRow = document.getElementById('job-' + j.id.toString()); 705 | jobRow.parentNode.removeChild(jobRow); 706 | if (_getTasksSize() === 0 && _getVehiclesSize() === 0) { 707 | LSetup.map.removeControl(clearControl); 708 | } 709 | checkControls(); 710 | break; 711 | } 712 | } 713 | _recomputeBounds(); 714 | } 715 | 716 | var _removeShipment = function(s) { 717 | _clearSolution(); 718 | for (var type of ['pickup', 'delivery']) { 719 | LSetup.map.removeLayer(data.markers[type][s[type].id.toString()]); 720 | delete data.markers[type][s[type].id.toString()]; 721 | } 722 | delete data.pdLines[s.pickup.id + '-' + s.delivery.id]; 723 | 724 | for (var i = 0; i < data.shipments.length; i++) { 725 | if (data.shipments[i].pickup.id == s.pickup.id && 726 | data.shipments[i].delivery.id == s.delivery.id) { 727 | data.shipments.splice(i, 1); 728 | 729 | var pickupRow = document.getElementById('pickup-' + s.pickup.id.toString()); 730 | pickupRow.parentNode.removeChild(pickupRow); 731 | var deliveryRow = document.getElementById('delivery-' + s.delivery.id.toString()); 732 | deliveryRow.parentNode.removeChild(deliveryRow); 733 | 734 | if (_getTasksSize() === 0 && _getVehiclesSize() === 0) { 735 | LSetup.map.removeControl(clearControl); 736 | } 737 | checkControls(); 738 | break; 739 | } 740 | } 741 | _recomputeBounds(); 742 | } 743 | 744 | var _removeStart = function(v) { 745 | var allowRemoval = (data.vehicles.length > 1) || v.end; 746 | if (allowRemoval) { 747 | _clearSolution(); 748 | 749 | LSetup.map.removeLayer(data.vehiclesMarkers[v.id.toString() + '_start']); 750 | delete data.vehiclesMarkers[v.id.toString()]; 751 | 752 | for (var i = 0; i < data.vehicles.length; i++) { 753 | if (data.vehicles[i].id == v.id) { 754 | delete data.vehicles[i].start; 755 | delete data.vehicles[i].startDescription; 756 | if (!v.end) { 757 | var vTable = document.getElementById('panel-vehicles-' + v.id.toString()); 758 | vTable.parentNode.removeChild(vTable); 759 | data.vehicles.splice(i, 1); 760 | _updateAllJobPopups(); 761 | } 762 | break; 763 | } 764 | } 765 | 766 | _recomputeBounds(); 767 | } else { 768 | alert('Can\'t delete both start and end with a single vehicle.'); 769 | } 770 | return allowRemoval; 771 | } 772 | 773 | var _removeEnd = function(v) { 774 | var allowRemoval = (data.vehicles.length > 1) || v.start; 775 | if (allowRemoval) { 776 | _clearSolution(); 777 | 778 | LSetup.map.removeLayer(data.vehiclesMarkers[v.id.toString() + '_end']); 779 | delete data.vehiclesMarkers[v.id.toString()]; 780 | 781 | for (var i = 0; i < data.vehicles.length; i++) { 782 | if (data.vehicles[i].id == v.id) { 783 | delete data.vehicles[i].end; 784 | delete data.vehicles[i].endDescription; 785 | if (!v.start) { 786 | var vTable = document.getElementById('panel-vehicles-' + v.id.toString()); 787 | vTable.parentNode.removeChild(vTable); 788 | data.vehicles.splice(i, 1); 789 | _updateAllJobPopups(); 790 | } 791 | break; 792 | } 793 | } 794 | 795 | _recomputeBounds(); 796 | } else { 797 | alert('Can\'t delete both start and end with a single vehicle.'); 798 | } 799 | return allowRemoval; 800 | } 801 | 802 | var showStart = function(v, center) { 803 | var k = v.id.toString() + '_start'; 804 | data.vehiclesMarkers[k].openPopup(); 805 | if (center) { 806 | LSetup.map.panTo(data.vehiclesMarkers[k].getLatLng()); 807 | } 808 | } 809 | 810 | var showEnd = function(v, center) { 811 | var k = v.id.toString() + '_end'; 812 | data.vehiclesMarkers[k].openPopup(); 813 | if (center) { 814 | LSetup.map.panTo(data.vehiclesMarkers[k].getLatLng()); 815 | } 816 | } 817 | 818 | var setSolution = function(solution) { 819 | data.solution = solution; 820 | } 821 | 822 | var getSolution = function() { 823 | return data.solution; 824 | } 825 | 826 | var markUnassigned = function(unassigned) { 827 | for (var i = 0; i < unassigned.length; ++i) { 828 | data.markers[unassigned[i].type][unassigned[i].id.toString()] 829 | .setStyle(LSetup.markerStyle.unassigned); 830 | } 831 | } 832 | 833 | var addRoutes = function(resultRoutes) { 834 | for (var i = 0; i < resultRoutes.length; ++i) { 835 | var latlngs = polyUtil.decode(resultRoutes[i]['geometry']); 836 | 837 | var routeColor = LSetup.routeColors[i % LSetup.routeColors.length]; 838 | 839 | var path = new L.Polyline(latlngs, { 840 | opacity: LSetup.opacity, 841 | weight: LSetup.weight, 842 | color: routeColor}).addTo(LSetup.map); 843 | path.bindPopup('Vehicle ' + resultRoutes[i].vehicle.toString()); 844 | 845 | data.bounds.extend(latlngs); 846 | 847 | // Hide input task display. 848 | panelControl.hideTaskDisplay(); 849 | 850 | var solutionList = document.getElementById('panel-solution'); 851 | 852 | // Add vehicle to solution display 853 | var nb_rows = solutionList.rows.length; 854 | var row = solutionList.insertRow(nb_rows); 855 | row.title = 'Click to center the map'; 856 | 857 | var updateRouteOpacities = function (r, highOpacity, lowOpacity) { 858 | for (var k = 0; k < routes.length; k++) { 859 | if (k == r) { 860 | routes[k].setStyle({opacity: highOpacity}); 861 | } else { 862 | routes[k].setStyle({opacity: lowOpacity}); 863 | } 864 | } 865 | } 866 | 867 | var showRoute = function (r) { 868 | return function() { 869 | // Increase this route's opacity and decrease others. 870 | routes[r].openPopup(); 871 | routes[r].bringToFront(); 872 | updateRouteOpacities(r, LSetup.highOpacity, LSetup.lowOpacity); 873 | 874 | LSetup.map.fitBounds(routes[r].getBounds(), { 875 | paddingBottomRight: [panelControl.getWidth(), 0], 876 | paddingTopLeft: [50, 0], 877 | }); 878 | } 879 | }; 880 | 881 | path.on({ 882 | click: showRoute(i), 883 | popupclose: function() { 884 | for (var k = 0; k < routes.length; k++) { 885 | routes[k].setStyle({opacity: LSetup.opacity}); 886 | } 887 | } 888 | }); 889 | row.onclick = showRoute(i); 890 | 891 | var vCell = row.insertCell(0); 892 | vCell.setAttribute('class', 'vehicle-title'); 893 | vCell.setAttribute('colspan', 2); 894 | vCell.appendChild(document.createTextNode('Vehicle ' + resultRoutes[i].vehicle.toString())); 895 | 896 | var stepRank = 0; 897 | for (var s = 0; s < resultRoutes[i].steps.length; s++) { 898 | var step = resultRoutes[i].steps[s]; 899 | if (!actualSteps.includes(step.type)) { 900 | continue; 901 | } 902 | stepRank++; 903 | 904 | var stepId = step.id.toString(); 905 | 906 | data.markers[step.type][stepId].setStyle({color: routeColor}); 907 | 908 | // Add to solution display 909 | var nb_rows = solutionList.rows.length; 910 | var row = solutionList.insertRow(nb_rows); 911 | row.title = 'Click to center the map'; 912 | 913 | // Hack to make sure the marker index is right. 914 | var showCallback = function(type, id) { 915 | return function() { 916 | _openPopup(type, id); 917 | centerMarker(type, id); 918 | }; 919 | } 920 | row.onclick = showCallback(step.type, stepId); 921 | 922 | var idCell = row.insertCell(0); 923 | idCell.setAttribute('class', 'rank solution-display'); 924 | idCell.innerHTML = stepRank; 925 | 926 | var nameCell = row.insertCell(1); 927 | nameCell.appendChild(document.createTextNode(step.description)); 928 | } 929 | 930 | // Remember the path. This will cause hasSolution() to return true. 931 | routes.push(path); 932 | } 933 | 934 | fitView(); 935 | } 936 | 937 | /*** Events ***/ 938 | 939 | // Fit event. 940 | LSetup.map.on('fit', fitView); 941 | 942 | // Clear event. 943 | LSetup.map.on('clear', function() { 944 | // Remove controls. 945 | if (LSetup.map.fitControl) { 946 | LSetup.map.removeControl(LSetup.map.fitControl); 947 | } 948 | if (LSetup.map.clearControl) { 949 | LSetup.map.removeControl(LSetup.map.clearControl); 950 | } 951 | if (LSetup.map.solveControl) { 952 | LSetup.map.removeControl(LSetup.map.solveControl); 953 | } 954 | if (LSetup.map.summaryControl) { 955 | LSetup.map.removeControl(LSetup.map.summaryControl); 956 | } 957 | clearData(); 958 | 959 | // Delete locations display in the right panel. 960 | LSetup.map.panelControl.clearDisplay(); 961 | }); 962 | 963 | // Collapse panel. 964 | LSetup.map.on('collapse', function() { 965 | LSetup.map.collapseControl.toggle(); 966 | LSetup.map.panelControl.toggle(); 967 | }); 968 | 969 | /*** end Events ***/ 970 | 971 | var setData = function(data) { 972 | clearData(); 973 | 974 | for (var i = 0; i < data.vehicles.length; i++) { 975 | addVehicle(data.vehicles[i]); 976 | } 977 | 978 | if ('jobs' in data) { 979 | for (var i = 0; i < data.jobs.length; i++) { 980 | addJob(data.jobs[i]); 981 | } 982 | } 983 | 984 | if ('shipments' in data) { 985 | for (var i = 0; i < data.shipments.length; i++) { 986 | addShipment(data.shipments[i]); 987 | } 988 | } 989 | 990 | // Next user input should be a job. 991 | firstPlaceSet(); 992 | } 993 | 994 | var setOverpassData = function(data) { 995 | for (var i = 0; i < data.length; i++) { 996 | if (_getTasksSize() >= api.maxTaskNumber) { 997 | alert('Request too large: ' + (data.length - i).toString() + ' POI discarded.'); 998 | return; 999 | } 1000 | var job = { 1001 | id: data[i]['id'], 1002 | description: data[i]['tags']['name'] || data[i]['id'].toString(), 1003 | location: [data[i]['lon'], data[i]['lat']] 1004 | } 1005 | addJob(job); 1006 | } 1007 | } 1008 | 1009 | var loadSolution = function(data) { 1010 | if ('solution' in data) { 1011 | setSolution(data.solution); 1012 | } 1013 | } 1014 | 1015 | module.exports = { 1016 | fitView: fitView, 1017 | clearData: clearData, 1018 | getJobs: getJobs, 1019 | getShipments: getShipments, 1020 | getVehicles: getVehicles, 1021 | showStart: showStart, 1022 | setSolution: setSolution, 1023 | getSolution: getSolution, 1024 | addRoutes: addRoutes, 1025 | getNextTaskId: getNextTaskId, 1026 | getNextVehicleId: getNextVehicleId, 1027 | closeAllPopups: closeAllPopups, 1028 | isFirstPlace: isFirstPlace, 1029 | hasCapacity: hasCapacity, 1030 | firstPlaceSet: firstPlaceSet, 1031 | addVehicle: addVehicle, 1032 | markUnassigned: markUnassigned, 1033 | centerMarker: centerMarker, 1034 | addJob: addJob, 1035 | checkControls: checkControls, 1036 | setData: setData, 1037 | loadSolution: loadSolution, 1038 | getOverpassQuery: getOverpassQuery, 1039 | setOverpassData: setOverpassData 1040 | }; 1041 | -------------------------------------------------------------------------------- /src/utils/file_handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var api = require('../config/api'); 4 | var geocoder = require('./geocoder'); 5 | var address = require('./address'); 6 | var locationHandler = require('./locations'); 7 | var dataHandler = require('./data_handler'); 8 | var solutionHandler = require('./solution_handler'); 9 | var panelControl = require('../controls/panel'); 10 | 11 | var reader = new FileReader(); 12 | 13 | reader.onerror = function(event) { 14 | alert("File could not be read! Code " + event.target.error.code); 15 | }; 16 | 17 | reader.onload = function(event) { 18 | // We first try parsing the input to determine if the file contains 19 | // a valid json object with the expected keys. 20 | var validJsonInput = false; 21 | try { 22 | var data = JSON.parse(event.target.result); 23 | validJsonInput = 'vehicles' in data && Array.isArray(data.vehicles); 24 | hasValidJobs = 'jobs' in data && Array.isArray(data.jobs); 25 | hasValidShipments = 'shipments' in data && Array.isArray(data.shipments); 26 | validJsonInput &= (hasValidJobs || hasValidShipments); 27 | } catch(e) {} 28 | 29 | if (validJsonInput) { 30 | panelControl.hideInitDiv(); 31 | dataHandler.setData(data); 32 | dataHandler.closeAllPopups(); 33 | dataHandler.checkControls(); 34 | 35 | // Plot solution if current file contains one. 36 | if (('solution' in data) && ('code' in data['solution'])) { 37 | dataHandler.loadSolution(data); 38 | solutionHandler.plotSolution(); 39 | } 40 | 41 | dataHandler.fitView(); 42 | } else { 43 | // Start line by line parsing. 44 | var lines = event.target.result.split("\n"); 45 | 46 | // Strip blank lines from file. 47 | while (lines.indexOf("") > -1) { 48 | lines.splice(lines.indexOf(""), 1); 49 | } 50 | 51 | // Used to report after parsing the whole file. 52 | var context = { 53 | locNumber: 0, 54 | // The '1 +' accounts for the first job being actually the 55 | // start/end. 56 | targetLocNumber: Math.min(lines.length, 1 + api.maxTaskNumber), 57 | totalLocNumber: lines.length, 58 | unfoundLocs: [] 59 | }; 60 | 61 | for (var i = 0; i < context.targetLocNumber; ++i) { 62 | _batchGeocodeAdd(lines[i], context); 63 | } 64 | } 65 | }; 66 | 67 | var _batchGeocodeAdd = function(query, context) { 68 | geocoder.defaultGeocoder.geocode(query, function(results) { 69 | context.locNumber += 1; 70 | var r = results[0]; 71 | if (r) { 72 | locationHandler.addPlace(r.center, 73 | address.display(r)); 74 | } else { 75 | context.unfoundLocs.push(query); 76 | } 77 | if (context.locNumber === context.targetLocNumber) { 78 | // Last location have been tried. 79 | var msg = ''; 80 | if (context.targetLocNumber < context.totalLocNumber) { 81 | msg += 'Warning: only the first ' 82 | + context.targetLocNumber 83 | + ' locations where used.\n'; 84 | } 85 | if (context.unfoundLocs.length > 0) { 86 | msg += 'Unfound location(s):\n'; 87 | for (var i = 0; i < context.unfoundLocs.length; ++i) { 88 | msg += '- ' + context.unfoundLocs[i] + '\n'; 89 | } 90 | } 91 | dataHandler.fitView(); 92 | 93 | if (msg.length > 0) { 94 | alert(msg); 95 | } 96 | } 97 | }, context); 98 | }; 99 | 100 | var setFile = function() { 101 | var fileInput = document.getElementById('user-file'); 102 | fileInput.addEventListener("change", function(event) { 103 | reader.readAsText(fileInput.files[0]); 104 | }, false); 105 | } 106 | 107 | 108 | module.exports = { 109 | setFile: setFile 110 | }; 111 | 112 | -------------------------------------------------------------------------------- /src/utils/geocoder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('leaflet-control-geocoder'); 4 | 5 | var defaultGeocoder = L.Control.Geocoder.nominatim(); 6 | 7 | var control = L.Control.geocoder({ 8 | geocoder: defaultGeocoder, 9 | collapsed: true, 10 | position: 'topleft' 11 | }); 12 | 13 | module.exports = { 14 | defaultGeocoder: defaultGeocoder, 15 | control: control 16 | }; 17 | -------------------------------------------------------------------------------- /src/utils/locations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LSetup = require('../config/leaflet_setup'); 4 | var dataHandler = require('./data_handler'); 5 | var geocoder = require('./geocoder'); 6 | var address = require('./address'); 7 | var panelControl = require('../controls/panel'); 8 | 9 | // Add locations. 10 | var addPlace = function(latlng, name) { 11 | if (LSetup.maxBounds && !LSetup.maxBounds.contains(latlng)) { 12 | alert('Sorry, unsupported location. :-('); 13 | return; 14 | } 15 | panelControl.hideInitDiv(); 16 | 17 | if (dataHandler.isFirstPlace()) { 18 | // Add vehicle start/end. 19 | dataHandler.firstPlaceSet(); 20 | 21 | var addVechicleWithName = function(name, center) { 22 | var v = { 23 | 'id': dataHandler.getNextVehicleId(), 24 | 'start': [latlng.lng,latlng.lat], 25 | 'startDescription': name, 26 | 'end': [latlng.lng,latlng.lat], 27 | 'endDescription': name 28 | }; 29 | dataHandler.addVehicle(v); 30 | dataHandler.checkControls(); 31 | if (center) { 32 | dataHandler.showStart(v, center); 33 | } 34 | } 35 | 36 | if (name) { 37 | // Add vehicle with provided name for start and end. 38 | addVechicleWithName(name, true) 39 | } else { 40 | geocoder.defaultGeocoder.reverse(latlng, LSetup.map.options.crs.scale(19), function(results) { 41 | var r = results[0]; 42 | if (r) { 43 | // Add vehicle based on geocoding result for start and end. 44 | addVechicleWithName(address.display(r), false); 45 | } 46 | }); 47 | } 48 | panelControl.showOverpassDisplay(); 49 | } else { 50 | // Add regular job. 51 | var addJobWithName = function(name, center) { 52 | var j = { 53 | 'id': dataHandler.getNextTaskId(), 54 | 'description': name, 55 | 'location': [latlng.lng,latlng.lat] 56 | }; 57 | 58 | dataHandler.addJob(j); 59 | dataHandler.checkControls(); 60 | if (center) { 61 | dataHandler.centerMarker('job', j.id); 62 | } 63 | } 64 | 65 | if (name) { 66 | // Add job with provided name. 67 | addJobWithName(name, true); 68 | } else { 69 | geocoder.defaultGeocoder.reverse(latlng, LSetup.map.options.crs.scale(19), function(results) { 70 | var r = results[0]; 71 | if (r) { 72 | // Add job based on geocoding result. 73 | addJobWithName(address.display(r), false); 74 | } 75 | }); 76 | } 77 | } 78 | } 79 | 80 | module.exports = { 81 | addPlace: addPlace 82 | }; 83 | -------------------------------------------------------------------------------- /src/utils/overpass.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dataHandler = require('./data_handler'); 4 | var api = require('../config/api'); 5 | 6 | var query = function() { 7 | var request = dataHandler.getOverpassQuery(); 8 | var xhttp = new XMLHttpRequest(); 9 | xhttp.onreadystatechange = function() { 10 | if (xhttp.readyState == 4) { 11 | if (xhttp.status == 200) { 12 | applyResponse(JSON.parse(xhttp.response)); 13 | } else { 14 | alert('Error: ' + xhttp.status); 15 | } 16 | } 17 | }; 18 | xhttp.open('POST', api.overpassEndpoint, false); 19 | xhttp.setRequestHeader('Content-Type', 'application/json'); 20 | xhttp.send(request); 21 | dataHandler.closeAllPopups(); 22 | } 23 | 24 | var applyResponse = function(response) { 25 | dataHandler.setOverpassData(response['elements']); 26 | dataHandler.checkControls(); 27 | document.getElementById('wait-icon').removeAttribute('class'); 28 | } 29 | 30 | module.exports = { 31 | query: query, 32 | applyResponse: applyResponse 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/solution_handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dataHandler = require('./data_handler'); 4 | var api = require('../config/api'); 5 | var summaryControl = require('../controls/summary'); 6 | 7 | var solve = function() { 8 | // Format json input for solving. Use copies as we might want to 9 | // update amounts without messing initial objects. 10 | var input = { 11 | jobs: JSON.parse(JSON.stringify(dataHandler.getJobs())), 12 | shipments: JSON.parse(JSON.stringify(dataHandler.getShipments())), 13 | vehicles: JSON.parse(JSON.stringify(dataHandler.getVehicles())), 14 | "options":{ 15 | "g": true 16 | } 17 | }; 18 | 19 | if (!dataHandler.hasCapacity() && input.vehicles.length > 1) { 20 | for (var j = 0; j < input.jobs.length; j++) { 21 | input.jobs[j].delivery = [1]; 22 | } 23 | var C = Math.ceil(1.2 * input.jobs.length / input.vehicles.length); 24 | for (var v = 0; v < input.vehicles.length; v++) { 25 | input.vehicles[v].capacity = [C]; 26 | } 27 | } 28 | 29 | var xhttp = new XMLHttpRequest(); 30 | xhttp.onreadystatechange = function() { 31 | if (xhttp.readyState == 4) { 32 | document.getElementById('wait-icon').removeAttribute('class'); 33 | if (xhttp.status == 200) { 34 | dataHandler.setSolution(JSON.parse(xhttp.response)); 35 | plotSolution(); 36 | } else { 37 | alert('Error: ' + xhttp.status); 38 | } 39 | } 40 | }; 41 | var target = api.host; 42 | if (api.port) { 43 | target += ':' + api.port; 44 | } 45 | xhttp.open('POST', target, false); 46 | xhttp.setRequestHeader('Content-type', 'application/json'); 47 | xhttp.send(JSON.stringify(input)); 48 | dataHandler.closeAllPopups(); 49 | } 50 | 51 | var plotSolution = function() { 52 | var result = dataHandler.getSolution(); 53 | if (result['code'] !== 0) { 54 | alert(result['error']); 55 | return; 56 | } 57 | 58 | dataHandler.markUnassigned(result.unassigned); 59 | dataHandler.addRoutes(result.routes); 60 | dataHandler.checkControls(); 61 | summaryControl.update(result); 62 | } 63 | 64 | module.exports = { 65 | solve: solve, 66 | plotSolution: plotSolution 67 | }; 68 | -------------------------------------------------------------------------------- /src/utils/timing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var format = function (seconds) { 4 | var result = ''; 5 | var minutes = Math.round(seconds / 60); 6 | if (minutes < 60) { 7 | result = minutes + ' m'; 8 | } else { 9 | var hours = Math.floor(minutes / 60); 10 | var minutes_mod = minutes % 60; 11 | if (minutes_mod > 0) { 12 | var padding = (minutes_mod < 10) ? '0': ''; 13 | result = ' ' + padding + minutes_mod + result; 14 | } 15 | if (hours > 0) { 16 | result = (hours % 24) + ' h' + result; 17 | } 18 | var days = Math.floor(hours / 24); 19 | if (days > 0) { 20 | result = (days % 7) + ' d ' + result; 21 | } 22 | var weeks = Math.floor(days / 7); 23 | if (weeks > 0) { 24 | result = weeks + ' w ' + result; 25 | } 26 | } 27 | return result; 28 | } 29 | 30 | module.exports = { 31 | format: format 32 | }; 33 | --------------------------------------------------------------------------------