diff --git a/composer.json b/composer.json
index 7219d070..d2f81e89 100644
--- a/composer.json
+++ b/composer.json
@@ -25,6 +25,7 @@
"symfony/framework-bundle": "^4.0",
"symfony/maker-bundle": "^1.0",
"symfony/orm-pack": "^1.0",
+ "symfony/process": "^4.0",
"symfony/profiler-pack": "^1.0",
"symfony/security-bundle": "^4.0",
"symfony/translation": "^4.0",
diff --git a/composer.lock b/composer.lock
index ca8a10a1..1e76e914 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "content-hash": "b101ecfbc1f6f2270f0e8ad326035b7e",
+ "content-hash": "f03b92d48946e8b2ee19466f931c826f",
"packages": [
{
"name": "catalyst/auth-bundle",
@@ -4143,6 +4143,55 @@
],
"time": "2019-11-27T16:25:15+00:00"
},
+ {
+ "name": "symfony/process",
+ "version": "v4.4.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/c714958428a85c86ab97e3a0c96db4c4f381b7f5",
+ "reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.4-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Process Component",
+ "homepage": "https://symfony.com",
+ "time": "2020-05-30T20:06:45+00:00"
+ },
{
"name": "symfony/profiler-pack",
"version": "v1.0.4",
diff --git a/src/Controller/AnalyticsController.php b/src/Controller/AnalyticsController.php
index 8dfbddde..dbbdbf29 100644
--- a/src/Controller/AnalyticsController.php
+++ b/src/Controller/AnalyticsController.php
@@ -8,6 +8,8 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Process\Process;
+use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
@@ -22,6 +24,53 @@ use App\Entity\Hub;
class AnalyticsController extends Controller
{
+ protected $weekdays = [
+ 'Monday',
+ 'Tuesday',
+ 'Wednesday',
+ 'Thursday',
+ 'Friday',
+ 'Saturday',
+ 'Sunday',
+ ];
+
+ protected $day_shifts = [
+ ['Mon - Sat', 0, 1, 2, 3, 4, 5], // Mon - Sat
+ ['Tue - Sun', 1, 2, 3, 4, 5, 6], // Tue - Sun
+ ['Wed - Mon', 2, 3, 4, 5, 6, 0], // Wed - Mon
+ ['Thu - Tue', 3, 4, 5, 6, 0, 1], // Thu - Tue
+ ['Fri - Wed', 4, 5, 6, 0, 1, 2], // Fri - Wed
+ ['Sat - Thu', 5, 6, 0, 1, 2, 3], // Sat - Thu
+ ['Sun - Fri', 6, 0, 1, 2, 3, 4], // Sun - Fri
+ ];
+
+ protected $hour_shifts = [
+ ['00:00 - 09:00', 0, 1, 2, 3, 4, 5, 6, 7, 8],
+ ['01:00 - 10:00', 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ ['02:00 - 11:00', 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ ['03:00 - 12:00', 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ ['04:00 - 13:00', 4, 5, 6, 7, 8, 9, 10, 11, 12],
+ ['05:00 - 14:00', 5, 6, 7, 8, 9, 10, 11, 12, 13],
+ ['06:00 - 15:00', 6, 7, 8, 9, 10, 11, 12, 13, 14],
+ ['07:00 - 16:00', 7, 8, 9, 10, 11, 12, 13, 14, 15],
+ ['08:00 - 17:00', 8, 9, 10, 11, 12, 13, 14, 15, 16],
+ ['09:00 - 18:00', 9, 10, 11, 12, 13, 14, 15, 16, 17],
+ ['10:00 - 19:00', 10, 11, 12, 13, 14, 15, 16, 17, 18],
+ ['11:00 - 20:00', 11, 12, 13, 14, 15, 16, 17, 18, 19],
+ ['12:00 - 21:00', 12, 13, 14, 15, 16, 17, 18, 19, 20],
+ ['13:00 - 22:00', 13, 14, 15, 16, 17, 18, 19, 20, 21],
+ ['14:00 - 23:00', 14, 15, 16, 17, 18, 19, 20, 21, 22],
+ ['15:00 - 00:00', 15, 16, 17, 18, 19, 20, 21, 22, 23],
+ ['16:00 - 01:00', 16, 17, 18, 19, 20, 21, 22, 23, 0],
+ ['17:00 - 02:00', 17, 18, 19, 20, 21, 22, 23, 0, 1],
+ ['18:00 - 03:00', 18, 19, 20, 21, 22, 23, 0, 1, 2],
+ ['19:00 - 04:00', 19, 20, 21, 22, 23, 0, 1, 2, 3],
+ ['20:00 - 05:00', 20, 21, 22, 23, 0, 1, 2, 3, 4],
+ ['21:00 - 06:00', 21, 22, 23, 0, 1, 2, 3, 4, 5],
+ ['22:00 - 07:00', 22, 23, 0, 1, 2, 3, 4, 5, 6],
+ ['23:00 - 08:00', 23, 0, 1, 2, 3, 4, 5, 6, 7],
+ ];
+
/**
* @Menu(selected="analytics_forecast")
* @IsGranted("analytics.forecast")
@@ -56,19 +105,21 @@ class AnalyticsController extends Controller
$hub_list = $req->request->get('hub_ids', []);
$distances = $req->request->get('distances', []);
$today = DateTime::createFromFormat('d M Y', $req->request->get('date'));
- error_log(print_r($hub_list, true));
+ $month = $today->format('m');
+ // error_log(print_r($hub_list, true));
// $hub_list = [ 6, 4, 36, 7, 8, 126, 127, 18, 12, 9, 60, 10, 21, 135 ];
$hub_data = [];
$hub_coverage = [];
+ $overlaps = [];
foreach ($hub_list as $key => $hub_id)
{
+ $dist = $distances[$key];
$hub = $em->getRepository(Hub::class)->find($hub_id);
$coords = $hub->getCoordinates();
- $dist = $distances[$key];
- $hub_data[$hub_id] = $this->generateHubData($em, $hub, $dist, $today);
+ $hub_data[$hub_id] = $this->generateHubData($em, $hub, $dist, $today, $overlaps);
$hub_coverage[] = [
'longitude' => $coords->getLongitude(),
@@ -77,16 +128,156 @@ class AnalyticsController extends Controller
];
}
+ // reprocess weekday data to account for overlap
+ foreach ($hub_data as $hub_id => $one_hub)
+ {
+ $c_weekday = $one_hub['c_weekday'];
+ $chart_weekday = $this->generateWeekdayData($c_weekday, $today, $overlaps);
+ $chart_all_weekdays = $this->generateAllWeekData($c_weekday, $today, $overlaps);
+
+ // figure out the rider schedules based on the max hour values
+ $scheduler_data = $chart_all_weekdays['scheduler_data'];
+ // error_log(print_r($scheduler_data, true));
+ unset($chart_all_weekdays['scheduler_data']);
+
+ // run scheduler
+ // send 2018 + month data
+ $sched_res = $this->runScheduler($scheduler_data['2018'][$month]);
+
+ // tally total JOs for the month
+ $total_jos = 0;
+ foreach ($scheduler_data['2018'][$month] as $sday_data)
+ {
+ foreach ($sday_data as $shour_data)
+ {
+ $total_jos += $shour_data;
+ }
+ }
+
+ $hub_data[$hub_id]['data_weekday'] = $chart_weekday;
+ $hub_data[$hub_id]['data_all_weekdays'] = $chart_all_weekdays;
+ $hub_data[$hub_id]['data_shift'] = $sched_res['weekday_shifts'];
+ $hub_data[$hub_id]['total_jos'] = $total_jos;
+ $hub_data[$hub_id]['total_riders'] = $sched_res['total_riders'];
+
+ unset($hub_data[$hub_id]['c_weekday']);
+ }
+
+ // error_log(print_r($overlaps, true));
+
+ // get job orders not covered by hubs
+ $not_covered = $this->generateNotCoveredData($em, $hub_coverage, $today);
+
$params = [
'date' => $today,
'hub_list' => $hub_data,
'hub_coverage' => $hub_coverage,
+ 'not_covered' => $not_covered,
];
return $this->render('analytics/forecast_submit.html.twig', $params);
}
- protected function generateHubData($em, $hub, $distance_limit, DateTime $today)
+ protected function runScheduler($scheduler_data)
+ {
+ // run python script to solve scheduling for riders
+
+ $python_cmd = '/usr/bin/python3';
+ $sched_script = __DIR__ . '/../../utils/schedule_solver/solver.py';
+
+ // go through the days
+ $args = [
+ $python_cmd,
+ $sched_script,
+ ];
+ foreach ($scheduler_data as $weekday_data)
+ $args[] = implode('-', $weekday_data);
+
+ error_log(print_r($args, true));
+
+ // error_log('running...' . $sched_script);
+
+ $proc = new Process($args);
+ $proc->run();
+
+ if (!$proc->isSuccessful())
+ error_log('SCHEDULER DID NOT RUN PROPERLY');
+
+ $res = $proc->getOutput();
+ error_log($res);
+
+
+ // segregate into weekdays
+ $day_data = [];
+ $i = 0;
+ foreach ($scheduler_data as $weekday_data)
+ {
+ $total_jos = 0;
+ foreach ($weekday_data as $hourly_jo)
+ $total_jos += $hourly_jo;
+
+ $day_data[$i] = [
+ 'weekday' => $this->weekdays[$i],
+ 'total_jos' => $total_jos,
+ 'total_riders' => 0,
+ 'shifts' => [],
+ ];
+
+ $i++;
+ }
+
+ $shifts = [];
+ $res_lines = explode("\n", $res);
+ $total_riders = 0;
+ foreach ($res_lines as $line)
+ {
+ // format is day shift - hour shift - number of riders
+ $shift_data = explode('-', $line);
+ if (count($shift_data) != 3)
+ continue;
+
+ $day_shift_index = $shift_data[0];
+ $hour_shift_index = $shift_data[1];
+ $rider_count = $shift_data[2];
+
+ $total_riders += $rider_count;
+
+ $label = $this->day_shifts[$day_shift_index][0] . ' ' . $this->hour_shifts[$hour_shift_index][0];
+
+ // initialize hours
+ $rider_hours = [];
+ for ($i = 0; $i < 24; $i++)
+ $rider_hours[$i] = 0;
+ for ($i = 1; $i < count($this->hour_shifts[$hour_shift_index]); $i++)
+ $rider_hours[$this->hour_shifts[$hour_shift_index][$i]] = 1;
+
+ error_log('allocating ' . $rider_count . ' for ' . $label);
+
+ // add shifts to the weekday
+ for ($i = 1; $i < count($this->day_shifts[$day_shift_index]); $i++)
+ {
+ $day = $this->day_shifts[$day_shift_index][$i];
+
+ $day_data[$day]['shifts'][] = [
+ 'label' => $label,
+ 'count' => $rider_count,
+ 'hours' => $rider_hours,
+ ];
+
+ $day_data[$day]['total_riders'] += $rider_count;
+ }
+ }
+
+
+ $data = [
+ 'weekday_shifts' => $day_data,
+ 'total_riders' => $total_riders,
+ ];
+
+ return $data;
+ }
+
+ protected function generateHubData($em, $hub, $distance_limit, DateTime $today, &$overlaps)
{
$date_start = DateTime::createFromFormat('Y-m-d H:i:s', '2018-01-01 00:00:00');
$date_end = new DateTime();
@@ -95,7 +286,7 @@ class AnalyticsController extends Controller
// $hub = $em->getRepository(Hub::class)->find($hub_id);
$conn = $em->getConnection();
- // get job order data
+ // get job order data (job orders within coverage area)
$jos = $this->generateJobOrderData($conn, $hub, $distance_limit);
// initialize counters
@@ -116,6 +307,9 @@ class AnalyticsController extends Controller
$hour = $date->format('H');
$week = $date->format('W');
+ $jo_id = $jo['id'];
+ $hub_id = $hub->getID();
+
// year day
if (!isset($c_day[$year][$month][$day]))
$c_day[$year][$month][$day] = 0;
@@ -126,9 +320,11 @@ class AnalyticsController extends Controller
{
$c_weekday[$year][$month][$weekday][$hour]['total'] = 0;
$c_weekday[$year][$month][$weekday][$hour]['count'] = 0;
+ $c_weekday[$year][$month][$weekday][$hour]['jos'] = [];
}
$c_weekday[$year][$month][$weekday][$hour]['total']++;
+ $c_weekday[$year][$month][$weekday][$hour]['jos'][$jo_id] = $jo_id;
if (!isset($c_week_count[$year][$month][$week][$weekday][$hour]))
{
@@ -136,12 +332,19 @@ class AnalyticsController extends Controller
$c_week_count[$year][$month][$week][$weekday][$hour] = 1;
$c_weekday[$year][$month][$weekday][$hour]['count']++;
}
+
+ // track overlaps (jo that can be handled by more than one hub)
+ if (!isset($overlaps[$jo_id]))
+ {
+ $overlaps[$jo_id] = [];
+ }
+ $overlaps[$jo_id][$hub_id] = $hub_id;
}
// error_log(print_r($c_weekday, true));
$chart_year = $this->generateYearData($date_start, $date_end, $c_day);
- $chart_weekday = $this->generateWeekdayData($c_weekday, $today);
+ // $chart_weekday = $this->generateWeekdayData($c_weekday, $today);
// error_log(print_r($chart_weekday, true));
@@ -149,7 +352,9 @@ class AnalyticsController extends Controller
'id' => $hub->getID(),
'label' => $hub->getName(),
'data_year' => $chart_year,
- 'data_weekday' => $chart_weekday,
+ // 'data_weekday' => $chart_weekday,
+ 'c_weekday' => $c_weekday, // sending raw weekday data because we need to process overlaps
+ // TODO: refactor this pls
];
return $params;
@@ -212,7 +417,69 @@ class AnalyticsController extends Controller
return $chart_year;
}
- protected function generateWeekdayData($all_weekday_data, $today)
+ protected function generateAllWeekData($all_weekday_data, $today, $overlaps)
+ {
+ $data = [];
+
+ // build hours
+ $hours = [];
+ for ($i = 0; $i < 24; $i++)
+ $hours[] = sprintf('%02d', $i);
+
+ $month = $today->format('m');
+ $year_data = [];
+ $scheduler_data = [];
+
+ // gather maximum for each hour
+ foreach ($this->weekdays as $weekday)
+ {
+ foreach ($all_weekday_data as $year => $year_data)
+ {
+ // go through the hours
+ foreach ($hours as $hour)
+ {
+ $id = $hour + 0;
+ if (!isset($data[$id]))
+ $data[$id] = [
+ 'hour' => $hour,
+ ];
+
+ // get hour data
+ $year_id = 'y' . $year;
+ $prefix = $year_id . '_' . $weekday;
+ if (isset($year_data[$month][$weekday][$hour]))
+ {
+ // calculate the rider value for each JO and use that score as basis
+ $total_rv = $this->calculateTotalRiderValue($year_data[$month][$weekday][$hour]['jos'], $overlaps);
+ $rv_average = ceil($total_rv / $year_data[$month][$weekday][$hour]['count']);
+
+ $data[$id][$prefix] = $year_data[$month][$weekday][$hour]['total'];
+ $data[$id][$prefix . '_count'] = $year_data[$month][$weekday][$hour]['count'];
+ $data[$id][$prefix . '_average'] = ceil($year_data[$month][$weekday][$hour]['total'] / $year_data[$month][$weekday][$hour]['count']);
+ $data[$id][$prefix . '_rv_average'] = $rv_average;
+
+ // assign scheduler data
+ $scheduler_data[$year][$month][$weekday][$hour] = $rv_average;
+ }
+ else
+ {
+ if (!isset($scheduler_data[$year][$month][$weekday][$hour]))
+ {
+ $data[$id][$prefix . '_rv_average'] = 0;
+ $scheduler_data[$year][$month][$weekday][$hour] = 0;
+ }
+ }
+ }
+ }
+ }
+
+ $data['scheduler_data'] = $scheduler_data;
+
+ // error_log(print_r($data, true));
+ return $data;
+ }
+
+ protected function generateWeekdayData($all_weekday_data, $today, $overlaps)
{
$data = [];
@@ -241,13 +508,73 @@ class AnalyticsController extends Controller
{
$year_id = 'y' . $year;
+ // calculate the rider value for each JO and use that score as basis
+ $total_rv = $this->calculateTotalRiderValue($year_data[$month][$weekday][$hour]['jos'], $overlaps);
+
$data[$id][$year_id] = $year_data[$month][$weekday][$hour]['total'];
$data[$id][$year_id . '_count'] = $year_data[$month][$weekday][$hour]['count'];
$data[$id][$year_id . '_average'] = ceil($year_data[$month][$weekday][$hour]['total'] / $year_data[$month][$weekday][$hour]['count']);
+ $data[$id][$year_id . '_rv_average'] = ceil($total_rv / $year_data[$month][$weekday][$hour]['count']);
}
}
}
return $data;
}
+
+ protected function calculateTotalRiderValue($jos, $overlaps)
+ {
+ // rider value = 1 / number of hubs (overlaps) that can service JO
+
+ $sum = 0.0;
+ $jo_count = count($jos);
+
+ foreach ($jos as $jo_id)
+ {
+ $hub_count = count($overlaps[$jo_id]);
+ $rv = 1 / $hub_count;
+ $sum += $rv;
+ }
+
+ return $sum;
+ }
+
+ protected function generateNotCoveredData($em, $hub_coverage, $today)
+ {
+ $conn = $em->getConnection();
+
+ $month = $today->format('m');
+ $weekday = $today->format('N') - 1;
+
+ $wheres = [];
+ foreach ($hub_coverage as $hub_data)
+ {
+ $long = $hub_data['longitude'];
+ $lat = $hub_data['latitude'];
+ $dist = $hub_data['distance'];
+
+ // get areas not covered
+ $wheres[] = "st_distance(coordinates, Point($long, $lat)) * 111195 > $dist";
+ }
+
+ $where_string = implode(' and ', $wheres);
+
+ $sql = "select st_x(coordinates) as longitude, st_y(coordinates) as latitude, id, date_schedule from job_order where $where_string and status <> 'cancelled' and month(date_schedule) = $month and weekday(date_schedule) = $weekday order by date_schedule asc";
+ $stmt = $conn->prepare($sql);
+ $stmt->execute();
+ $jos = $stmt->fetchAll();
+
+ // error_log($sql);
+ // error_log(print_r($jos, true));
+
+ return $jos;
+ }
+
+ protected function generateRiderSchedule()
+ {
+ }
+
+ protected function solveRiderSchedule()
+ {
+ }
}
diff --git a/symfony.lock b/symfony.lock
index 116ed675..900434cc 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -260,6 +260,9 @@
"symfony/polyfill-php73": {
"version": "v1.11.0"
},
+ "symfony/process": {
+ "version": "v4.4.9"
+ },
"symfony/profiler-pack": {
"version": "v1.0.3"
},
diff --git a/templates/analytics/forecast_submit.html.twig b/templates/analytics/forecast_submit.html.twig
index 3951a231..2fafd57f 100644
--- a/templates/analytics/forecast_submit.html.twig
+++ b/templates/analytics/forecast_submit.html.twig
@@ -2,6 +2,26 @@
{% block stylesheets %}
+
{% endblock %}
{% block body %}
@@ -38,6 +58,90 @@
+
+
+
+
+
+
+
+ {% for day_data in hub.data_shift %}
+
+
+
+
+ | {{ day_data.weekday }} |
+ 00 |
+ 01 |
+ 02 |
+ 03 |
+ 04 |
+ 05 |
+ 06 |
+ 07 |
+ 08 |
+ 09 |
+ 10 |
+ 11 |
+ 12 |
+ 13 |
+ 14 |
+ 15 |
+ 16 |
+ 17 |
+ 18 |
+ 19 |
+ 20 |
+ 21 |
+ 22 |
+ 23 |
+
+
+
+ {% for shift_data in day_data.shifts %}
+ {% for i in 1..shift_data.count %}
+
+ | {{ shift_data.label }} |
+ {% for hour_coverage in shift_data.hours %}
+ {% if hour_coverage %}
+ |
+ {% else %}
+ |
+ {% endif %}
+ {% endfor %}
+
+ {% endfor %}
+ {% endfor %}
+
+
+
+ {% endfor %}
+
+
+
+ | Day |
+ # JO |
+ # Rider |
+ JO per Rider |
+
+
+
+ {% for i in 0..6 %}
+
+ | {{ hub.data_shift[i].weekday }} |
+ {{ hub.data_shift[i].total_jos }} |
+ {{ hub.data_shift[i].total_riders }} |
+ {{ (hub.data_shift[i].total_jos / hub.data_shift[i].total_riders) | round(1, 'common') }} |
+
+ {% endfor %}
+
+ | Overall |
+ {{ hub.total_jos }} |
+ {{ hub.total_riders }} |
+ {{ (hub.total_jos / hub.total_riders) | round(1, 'common') }} |
+
+
+
@@ -67,6 +171,15 @@ var map = L.map('map_coverage').setView([{% trans %}default_lat{% endtrans %}, {
L.circle([{{ cover.latitude }}, {{ cover.longitude }}], { radius: {{ cover.distance }} }).addTo(map);
{% endfor %}
+//------------------------------------------------------------------------
+
+// display not covered
+{% for jo_na in not_covered %}
+L.marker([{{ jo_na.latitude }}, {{ jo_na.longitude }}]).addTo(map);
+{% endfor %}
+
+//------------------------------------------------------------------------
+
{% for hub in hub_list %}
// create chart instance
var chart = am4core.create("year-day-chart-{{ hub.id }}", am4charts.XYChart);
@@ -110,6 +223,7 @@ s2020.dataFields.categoryX = "date";
var chart2 = am4core.create("month-weekday-chart-{{ hub.id }}", am4charts.XYChart);
chart2.data = {{ hub.data_weekday|json_encode|raw }};
+chart2.legend = new am4charts.Legend();
var xAxis = chart2.xAxes.push(new am4charts.CategoryAxis());
xAxis.dataFields.category = "hour";
@@ -132,6 +246,95 @@ l2019.strokeWidth = 3;
l2019.dataFields.valueY = "y2019_average";
l2019.dataFields.categoryX = "hour";
+//------------------------------------------------------------------------
+
+var chart2 = am4core.create("month-weekday-chart-rv-{{ hub.id }}", am4charts.XYChart);
+chart2.data = {{ hub.data_weekday|json_encode|raw }};
+chart2.legend = new am4charts.Legend();
+
+var xAxis = chart2.xAxes.push(new am4charts.CategoryAxis());
+xAxis.dataFields.category = "hour";
+xAxis.title.text = "Hour";
+var yAxis = chart2.yAxes.push(new am4charts.ValueAxis());
+yAxis.title.text = "Orders";
+yAxis.maxPrecision = 0;
+
+var l2018 = chart2.series.push(new am4charts.LineSeries());
+l2018.name = "2018";
+l2018.stroke = am4core.color("#0000FF");
+l2018.strokeWidth = 2;
+l2018.dataFields.valueY = "y2018_rv_average";
+l2018.dataFields.categoryX = "hour";
+
+var l2019 = chart2.series.push(new am4charts.LineSeries());
+l2019.name = "2019";
+l2019.stroke = am4core.color("#FF0000");
+l2019.strokeWidth = 3;
+l2019.dataFields.valueY = "y2019_rv_average";
+l2019.dataFields.categoryX = "hour";
+
+//------------------------------------------------------------------------
+
+var chart2 = am4core.create("month-all-weekday-chart-{{ hub.id }}", am4charts.XYChart);
+chart2.data = {{ hub.data_all_weekdays|json_encode|raw }};
+chart2.legend = new am4charts.Legend();
+
+var xAxis = chart2.xAxes.push(new am4charts.CategoryAxis());
+xAxis.dataFields.category = "hour";
+xAxis.title.text = "Hour";
+var yAxis = chart2.yAxes.push(new am4charts.ValueAxis());
+yAxis.title.text = "Orders";
+yAxis.maxPrecision = 0;
+
+var lmon = chart2.series.push(new am4charts.LineSeries());
+lmon.name = "Monday";
+lmon.stroke = am4core.color("#003f5c");
+lmon.strokeWidth = 2;
+lmon.dataFields.valueY = "y2018_Monday_rv_average";
+lmon.dataFields.categoryX = "hour";
+
+var l = chart2.series.push(new am4charts.LineSeries());
+l.name = "Tuesday";
+l.stroke = am4core.color("#374c80");
+l.strokeWidth = 2;
+l.dataFields.valueY = "y2018_Tuesday_rv_average";
+l.dataFields.categoryX = "hour";
+
+var l = chart2.series.push(new am4charts.LineSeries());
+l.name = "Wednesday";
+l.stroke = am4core.color("#7a5195");
+l.strokeWidth = 2;
+l.dataFields.valueY = "y2018_Wednesday_rv_average";
+l.dataFields.categoryX = "hour";
+
+var l = chart2.series.push(new am4charts.LineSeries());
+l.name = "Thursday";
+l.stroke = am4core.color("#bc5090");
+l.strokeWidth = 2;
+l.dataFields.valueY = "y2018_Thursday_rv_average";
+l.dataFields.categoryX = "hour";
+
+var l = chart2.series.push(new am4charts.LineSeries());
+l.name = "Friday";
+l.stroke = am4core.color("#ef5675");
+l.strokeWidth = 2;
+l.dataFields.valueY = "y2018_Friday_rv_average";
+l.dataFields.categoryX = "hour";
+
+var l = chart2.series.push(new am4charts.LineSeries());
+l.name = "Saturday";
+l.stroke = am4core.color("#ff764a");
+l.strokeWidth = 2;
+l.dataFields.valueY = "y2018_Saturday_rv_average";
+l.dataFields.categoryX = "hour";
+
+var l = chart2.series.push(new am4charts.LineSeries());
+l.name = "Sunday";
+l.stroke = am4core.color("#ffa600");
+l.strokeWidth = 2;
+l.dataFields.valueY = "y2018_Sunday_rv_average";
+l.dataFields.categoryX = "hour";
+
{% endfor %}
diff --git a/utils/schedule_solver/solver.py b/utils/schedule_solver/solver.py
new file mode 100644
index 00000000..f980cc44
--- /dev/null
+++ b/utils/schedule_solver/solver.py
@@ -0,0 +1,151 @@
+from __future__ import print_function
+from ortools.linear_solver import pywraplp
+import sys
+
+def main():
+
+ # days
+ days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+
+ # hours
+ hours = [
+ '00',
+ '01',
+ '02',
+ '03',
+ '04',
+ '05',
+ '06',
+ '07',
+ '08',
+ '09',
+ '10',
+ '11',
+ '12',
+ '13',
+ '14',
+ '15',
+ '16',
+ '17',
+ '18',
+ '19',
+ '20',
+ '21',
+ '22',
+ '23']
+
+ # initialize required hours - req_hours[days][hours]
+ req_hours = [[0 for x in range(len(hours))] for y in range(len(days))]
+
+ # get arguments
+ # there will be 7 arguments, monday to sunday schedule
+ for day_index in range(0, len(days)):
+ hours_string = sys.argv[day_index + 1]
+ hours_data = hours_string.split('-')
+ for hour_index in range(0, len(hours)):
+ req_hours[day_index][hour_index] = int(hours_data[hour_index])
+
+ # all hour shifts available
+ hour_shifts = [
+ ['00 - 09', 0, 1, 2, 3, 4, 5, 6, 7, 8],
+ ['01 - 10', 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ ['02 - 11', 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ ['03 - 12', 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ ['04 - 13', 4, 5, 6, 7, 8, 9, 10, 11, 12],
+ ['05 - 14', 5, 6, 7, 8, 9, 10, 11, 12, 13],
+ ['06 - 15', 6, 7, 8, 9, 10, 11, 12, 13, 14],
+ ['07 - 16', 7, 8, 9, 10, 11, 12, 13, 14, 15],
+ ['08 - 17', 8, 9, 10, 11, 12, 13, 14, 15, 16],
+ ['09 - 18', 9, 10, 11, 12, 13, 14, 15, 16, 17],
+ ['10 - 19', 10, 11, 12, 13, 14, 15, 16, 17, 18],
+ ['11 - 20', 11, 12, 13, 14, 15, 16, 17, 18, 19],
+ ['12 - 21', 12, 13, 14, 15, 16, 17, 18, 19, 20],
+ ['13 - 22', 13, 14, 15, 16, 17, 18, 19, 20, 21],
+ ['14 - 23', 14, 15, 16, 17, 18, 19, 20, 21, 22],
+ ['15 - 00', 15, 16, 17, 18, 19, 20, 21, 22, 23],
+ ['16 - 01', 16, 17, 18, 19, 20, 21, 22, 23, 0],
+ ['17 - 02', 17, 18, 19, 20, 21, 22, 23, 0, 1],
+ ['18 - 03', 18, 19, 20, 21, 22, 23, 0, 1, 2],
+ ['19 - 04', 19, 20, 21, 22, 23, 0, 1, 2, 3],
+ ['20 - 05', 20, 21, 22, 23, 0, 1, 2, 3, 4],
+ ['21 - 06', 21, 22, 23, 0, 1, 2, 3, 4, 5],
+ ['22 - 07', 22, 23, 0, 1, 2, 3, 4, 5, 6],
+ ['23 - 08', 23, 0, 1, 2, 3, 4, 5, 6, 7]]
+
+ # all possible days riders come in
+ day_shifts = [
+ ['Mon - Sat', 0, 1, 2, 3, 4, 5], # Mon - Sat
+ ['Tue - Sun', 1, 2, 3, 4, 5, 6], # Tue - Sun
+ ['Wed - Mon', 2, 3, 4, 5, 6, 0], # Wed - Mon
+ ['Thu - Tue', 3, 4, 5, 6, 0, 1], # Thu - Tue
+ ['Fri - Wed', 4, 5, 6, 0, 1, 2], # Fri - Wed
+ ['Sat - Thu', 5, 6, 0, 1, 2, 3], # Sat - Thu
+ ['Sun - Fri', 6, 0, 1, 2, 3, 4]] # Sun - Fri
+
+ # build shift lookup index
+
+
+ # instantiate glop solver
+ solver = pywraplp.Solver('SolveSchedule', pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)
+
+ # solver variables
+ solv_shifts = [[0 for x in range(len(hour_shifts))] for y in range(len(day_shifts))]
+
+ # objective
+ objective = solver.Objective()
+
+ # variables for shifts
+ for day_index in range(0, len(day_shifts)):
+ for hour_index in range(0, len(hour_shifts)):
+ solv_shifts[day_index][hour_index] = solver.IntVar(0, solver.infinity(), day_shifts[day_index][0] + ' ' + hour_shifts[hour_index][0])
+ # objective is to minimize number of shifts
+ objective.SetCoefficient(solv_shifts[day_index][hour_index], 1)
+ objective.SetMinimization()
+
+ # set constraints
+ constraints = [[0 for x in range(len(hours))] for y in range(len(days))]
+ # go through all days
+ for day_index in range(0, len(days)):
+ # go through all hours
+ for hour_index in range(0, len(hours)):
+ # hour personnel should be equal or less than shift personnel that covers those hours
+ # set the required manpower for that day + hour combo
+ # print('setting constraint for', day_index, '-', hour_index, '=', req_hours[day_index][hour_index])
+ constraints[day_index][hour_index] = solver.Constraint(req_hours[day_index][hour_index], solver.infinity())
+ # go through all day shifts
+ for shift_day in range(0, len(day_shifts)):
+ # go through days inside day shift
+ for shift_day_index in range(1, len(day_shifts[shift_day])):
+ # is day shift part of that day?
+ if day_index == day_shifts[shift_day][shift_day_index]:
+ # go through all hour shifts
+ for shift_hour in range(0, len(hour_shifts)):
+ # go through all hours inside hour shift
+ for shift_hour_index in range(1, len(hour_shifts[shift_hour])):
+ # is hour shift part of the hour
+ if hour_index == hour_shifts[shift_hour][shift_hour_index]:
+ # print(day_index, ' - ', hour_index, ' vs ', day_shifts[shift_day][shift_day_index], ' - ', hour_shifts[shift_hour][shift_hour_index])
+ # add it to constraint
+ constraints[day_index][hour_index].SetCoefficient(solv_shifts[shift_day][shift_hour], 1)
+
+ # solve it!
+ status = solver.Solve()
+ #print('Number of variables =', solver.NumVariables())
+ #print('Number of constraints =', solver.NumConstraints())
+
+ if status == solver.OPTIMAL:
+ #print('Optimal solution found!')
+ for day_index in range(0, len(day_shifts)):
+ for hour_index in range(0, len(hour_shifts)):
+ result = solv_shifts[day_index][hour_index].solution_value()
+ if result > 0:
+ print(day_index, hour_index, int(solv_shifts[day_index][hour_index].solution_value()), sep='-')
+ #print(day_shifts[day_index][0], ' ', hour_shifts[hour_index][0], ' = ', int(solv_shifts[day_index][hour_index].solution_value()), sep='')
+ else:
+ if status == solver.FEASIBLE:
+ print('Feasible solution found!')
+ else:
+ print('Could not solve problem.')
+
+if __name__ == '__main__':
+ main()