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, ]; 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')); // 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); // 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) { // 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); // 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] . ' ' . $this->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($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 = [ '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); // 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 // TODO: refactor this pls ]; return $params; } 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)); 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() { } }