├── .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 |
6 |
--------------------------------------------------------------------------------
/images/fa-icons/chevron-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/images/fa-icons/cog-alt.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/images/fa-icons/delete.svg:
--------------------------------------------------------------------------------
1 |
2 |
31 |
--------------------------------------------------------------------------------
/images/fa-icons/resize-small.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/images/fa-icons/trash.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/images/throbber.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VROOM-Project/vroom-frontend/2f5484661685dd383e6c5a914f8527b14a8eb774/images/throbber.gif
--------------------------------------------------------------------------------
/images/vroom.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 = '
';
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 |
--------------------------------------------------------------------------------