getRepository(Hub::class)->findAll(); $hub_list = []; foreach ($all_hubs as $hub) { $hub_list[$hub->getID()] = $hub->getName() . ' - ' . $hub->getBranch(); } $params = [ 'hub_list' => $hub_list, 'default_hubs' => $hub_ids, 'shift_schedules' => ShiftSchedule::getCollection(), ]; return $this->render('analytics/forecast_form.html.twig', $params); } /** * @Menu(selected="analytics_forecast") * @IsGranted("analytics.forecast") */ public function forecastSubmit(EntityManagerInterface $em, Request $req) { $today = new DateTime(); $hub_list = $req->request->get('hub_ids', []); $distances = $req->request->get('distances', []); $time_start = $req->request->get('time_from'); $time_end = $req->request->get('time_to'); $date_from = DateTime::createFromFormat('d M Y', $req->request->get('date_from')); $date_to = DateTime::createFromFormat('d M Y', $req->request->get('date_to')); $shift = $req->request->get('shift_schedule'); // TODO: populate the hour_shift array, depending on the shift selected $hour_shifts = $this->populateHourShift($shift); // error_log(print_r($hour_shifts, true)); // 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(); $hub_data[$hub_id] = $this->generateHubData($em, $hub, $dist, $date_from, $date_to, $time_start, $time_end, $overlaps); $hub_coverage[] = [ 'longitude' => $coords->getLongitude(), 'latitude' => $coords->getLatitude(), 'distance' => $dist, ]; } // init aggregate information $agg_data = []; for ($i = 0; $i < 7; $i++) { $agg_data[] = [ 'label' => $this->weekdays[$i], 'total_jos' => 0, 'total_riders' => 0, ]; } // 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, $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']); $total_jos = 0; // error_log(print_r($scheduler_data, true)); // run scheduler $sched_res = $this->runScheduler($scheduler_data, $hour_shifts, $shift); // tally total JOs for the month foreach ($scheduler_data as $sday_data) foreach ($sday_data as $shour_data) $total_jos += $shour_data; /* else { $sched_res = [ 'weekday_shifts' => $this->initDayData(), 'total_riders' => 0, ]; } */ // error_log(print_r($chart_all_weekdays, true)); // error_log(print_r($sched_res, true)); // agggregate weekday data $i = 0; foreach ($sched_res['weekday_shifts'] as $day_data) { $agg_data[$i]['total_jos'] += $day_data['total_jos']; $agg_data[$i]['total_riders'] += $day_data['total_riders']; $i++; } $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]['shift_summary'] = $sched_res['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); // error_log(print_r($agg_data, true)); $params = [ 'date' => $today, 'hub_list' => $hub_data, 'hub_coverage' => $hub_coverage, 'not_covered' => $not_covered, 'agg_data' => $agg_data, ]; return $this->render('analytics/forecast_submit.html.twig', $params); } protected function initDayData() { $day_data = []; // each weekday for ($i = 0; $i < 7; $i++) { $day_data[$i] = [ 'weekday' => $this->weekdays[$i], 'total_jos' => 0, 'total_riders' => 0, 'shifts' => [], ]; } return $day_data; } protected function runScheduler($scheduler_data, $hour_shifts, $shift) { // 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); // add shift $args[] = $shift; // error_log(print_r($args, true)); // error_log('running...' . $sched_script); $proc = new Process($args); $proc->run(); error_log('getErrorOutput() ' . $proc->getErrorOutput()); if (!$proc->isSuccessful()) error_log('SCHEDULER DID NOT RUN PROPERLY'); $res = $proc->getOutput(); // error_log($res); // returns lines with format: -- // segregate into weekdays $day_data = $this->initDayData(); $i = 0; foreach ($scheduler_data as $weekday_data) { $total_jos = 0; foreach ($weekday_data as $hourly_jo) $total_jos += $hourly_jo; $day_data[$i]['total_jos'] = $total_jos; $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] . ' ' . $hour_shifts[$hour_shift_index][0]; $shifts[] = [ 'label' => $label, 'count' => $rider_count, ]; // initialize hours $rider_hours = []; for ($i = 0; $i < 24; $i++) $rider_hours[$i] = 0; for ($i = 1; $i < count($hour_shifts[$hour_shift_index]); $i++) $rider_hours[$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 = [ 'shifts' => $shifts, 'weekday_shifts' => $day_data, 'total_riders' => $total_riders, ]; return $data; } protected function generateHubData($em, $hub, $distance_limit, DateTime $date_start, DateTime $date_end, $time_start, $time_end, &$overlaps) { // get hub to analyze // $hub = $em->getRepository(Hub::class)->find($hub_id); $conn = $em->getConnection(); // get job order data (job orders within coverage area) $jos = $this->generateJobOrderData($conn, $hub, $distance_limit, $date_start, $date_end, $time_start, $time_end); // get most bought battery from these JOs $batt_id = $this->getHubBattery($conn, $jos); $batt = $em->getRepository(Battery::class)->find($batt_id); if ($batt == null) $batt_data = [ 'mfg' => 'None', 'model' => 'None', 'size' => 'None', ]; else $batt_data = [ 'mfg' => $batt->getManufacturer()->getName(), 'model' => $batt->getModel()->getName(), 'size' => $batt->getSize()->getName(), ]; // initialize counters $c_weekday = []; $c_day = []; // counter to check instances of hourly weekdays, so we can get average $c_week_count = []; // loop through job orders foreach ($jos as $jo) { $date = DateTime::createFromFormat('Y-m-d H:i:s', $jo['date_schedule']); $year = $date->format('Y'); $month = $date->format('m'); $day = $date->format('d'); $weekday = $date->format('l'); $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; $c_day[$year][$month][$day]++; // weekday if (!isset($c_weekday[$weekday][$hour])) { $c_weekday[$weekday][$hour]['total'] = 0; $c_weekday[$weekday][$hour]['count'] = 0; $c_weekday[$weekday][$hour]['jos'] = []; } $c_weekday[$weekday][$hour]['total']++; $c_weekday[$weekday][$hour]['jos'][$jo_id] = $jo_id; // make a count of number of weeks, so we can take average if (!isset($c_week_count[$week][$weekday][$hour])) { // error_log('week detected - ' . $week); $c_week_count[$week][$weekday][$hour] = 1; $c_weekday[$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); // error_log(print_r($chart_weekday, true)); $params = [ 'id' => $hub->getID(), 'label' => $hub->getName(), 'data_year' => $chart_year, // 'data_weekday' => $chart_weekday, 'c_weekday' => $c_weekday, // sending raw weekday data because we need to process overlaps 'battery' => $batt_data, // TODO: refactor this pls ]; return $params; } protected function getHubBattery($conn, $jos) { // collect ids $ids = []; foreach ($jos as $jo) { $ids[] = $jo['id']; } // no jos, so no battery if (count($ids) <= 0) { return 0; } // ideally we encode the ids, but right now we're assuming they'll be int $in_text = implode(',', $ids); // get all the batteries ordered in these JOs $sql = 'select battery_id, count(*) as total from invoice i,invoice_item item where i.id = item.invoice_id and i.job_order_id in (' . $in_text . ') group by battery_id'; $stmt = $conn->prepare($sql); $stmt->execute(); $batteries = $stmt->fetchAll(); // error_log(print_r($batteries, true)); // get the most ordered, skipping the null battery $best_batt_id = 0; $best_batt_count = 0; foreach ($batteries as $batt) { if ($batt['battery_id'] == null) continue; // if current battery is better than our best if ($best_batt_count < $batt['total']) { $best_batt_id = $batt['battery_id']; $best_batt_count = $batt['total']; } } // error_log('BEST - ' . $best_batt_id . ' - ' . $best_batt_count); return $best_batt_id; } protected function generateJobOrderData($conn, $hub, $distance_limit, DateTime $date_start, DateTime $date_end, $time_start, $time_end) { $hub_coord = $hub->getCoordinates(); // create query // formula to convert to km is 111195 * st_distance $sql = "select id, round(st_distance(coordinates, Point(:lng, :lat)) * 111195) as dist, date_schedule from job_order where st_distance(coordinates, Point(:lng, :lat)) * 111195 <= :distance_limit and status <> 'cancelled' and date_schedule >= :date_start and date_schedule <= :date_end"; // check if time is specified if (!empty($time_start)) $sql .= ' and time(date_schedule) >= :time_start'; if (!empty($time_end)) $sql .= ' and time(date_schedule) <= :time_end'; $sql .= " order by date_schedule asc"; $stmt = $conn->prepare($sql); $stmt->bindValue('lng', $hub_coord->getLongitude()); $stmt->bindValue('lat', $hub_coord->getLatitude()); $stmt->bindValue('distance_limit', $distance_limit); $stmt->bindValue('date_start', $date_start->format('Y-m-d H:i:s')); $stmt->bindValue('date_end', $date_end->format('Y-m-d H:i:s')); if (!empty($time_start)) $stmt->bindValue('time_start', $time_start); if (!empty($time_end)) $stmt->bindValue('time_end', $time_end); $stmt->execute(); $jos = $stmt->fetchAll(); /* error_log(count($jos)); error_log(print_r($jos, true)); */ return $jos; } protected function generateYearData($date_start, $date_end, $c_day) { $res_year = []; $date_loop = clone $date_start; for (; $date_loop <= $date_end; $date_loop->add(new DateInterval('P1D'))) { $year = $date_loop->format('Y'); $month = $date_loop->format('m'); $day = $date_loop->format('d'); $year_field = 'y' . $date_loop->format('Y'); $id = $date_loop->format('m-d'); // NOTE: toss aside feb 29 // TODO: handle april 29 if ($id == '02-29') continue; if (!isset($res_year[$id][$year_field])) { $res_year[$id]['date'] = $date_loop->format('M j'); $res_year[$id][$year_field] = 0; } if (isset($c_day[$year][$month][$day])) $res_year[$id][$year_field] = $c_day[$year][$month][$day]; } // error_log(print_r($res_year, true)); $chart_year = []; foreach ($res_year as $day => $day_data) $chart_year[] = $day_data; return $chart_year; } protected function generateAllWeekData($all_weekday_data, $overlaps) { $data = []; // build hours $hours = []; for ($i = 0; $i < 24; $i++) $hours[] = sprintf('%02d', $i); // TODO: substitute this $year_data = $all_weekday_data; $scheduler_data = []; /* error_log('----------------------------------------------------------------------'); error_log(print_r($all_weekday_data, true)); error_log('----------------------------------------------------------------------'); */ // gather maximum for each hour foreach ($this->weekdays as $weekday) { // go through the hours foreach ($hours as $hour) { $id = $hour + 0; if (!isset($data[$id])) $data[$id] = [ 'hour' => $hour, ]; // get hour data $prefix = $weekday; if (isset($year_data[$weekday][$hour])) { // calculate the rider value for each JO and use that score as basis $total_rv = $this->calculateTotalRiderValue($year_data[$weekday][$hour]['jos'], $overlaps); $rv_average = ceil($total_rv / $year_data[$weekday][$hour]['count']); $data[$id][$prefix] = $year_data[$weekday][$hour]['total']; $data[$id][$prefix . '_count'] = $year_data[$weekday][$hour]['count']; $data[$id][$prefix . '_average'] = ceil($year_data[$weekday][$hour]['total'] / $year_data[$weekday][$hour]['count']); $data[$id][$prefix . '_rv_average'] = $rv_average; // assign scheduler data $scheduler_data[$weekday][$hour] = $rv_average; } else { if (!isset($scheduler_data[$weekday][$hour])) { $data[$id][$prefix . '_rv_average'] = 0; $scheduler_data[$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 = []; // build hours $hours = []; for ($i = 0; $i < 24; $i++) $hours[] = sprintf('%02d', $i); $month = $today->format('m'); $weekday = $today->format('l'); $year_data = []; 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 if (isset($year_data[$month][$weekday][$hour])) { $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() { } protected function populateHourShift($shift) { $hour_shift = []; if ($shift == '24_7') { $hour_shift = [ ['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], ]; } if ($shift == '8AM_5PM') { $hour_shift = [ ['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], ]; } if ($shift == '7AM_10PM') { $hour_shift = [ ['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] ]; } if ($shift == '6AM_7PM') { $hour_shift = [ ['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] ]; } if ($shift == '6AM_10PM') { $hour_shift = [ ['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] ]; } if ($shift == '6AM-12AM') { $hour_shift = [ ['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] ]; } return $hour_shift; } }