1use crate::format::coord_index::CoordIndex;
2use crate::format::problem::JobSkills as ApiJobSkills;
3use crate::format::problem::*;
4use crate::format::{JobIndex, Location};
5use crate::utils::VariableJobPermutation;
6use std::collections::HashMap;
7use std::sync::Arc;
8use vrp_core::{
9 construction::features::{
10 BreakPolicy, JobCompatibilityDimension, JobDemandDimension, JobGroupDimension, JobSkills as FeatureJobSkills,
11 JobSkillsDimension,
12 },
13 models::common::*,
14 models::problem::{
15 Actor, Fleet, Job, JobIdDimension, Jobs, Multi, Place, Single, TransportCost, VehicleIdDimension,
16 },
17 models::{Lock, LockDetail, LockOrder, LockPosition},
18};
19
20const MULTI_JOB_SAMPLE_SIZE: usize = 3;
22
23type PlaceData = (Option<Location>, Duration, Vec<TimeSpan>, Option<String>);
24type ApiJob = crate::format::problem::Job;
25
26pub(super) fn read_jobs_with_extra_locks(
27 api_problem: &ApiProblem,
28 props: &ProblemProperties,
29 coord_index: &CoordIndex,
30 fleet: &Fleet,
31 transport: &(dyn TransportCost + Sync + Send),
32 job_index: &mut JobIndex,
33 environment: &Environment,
34) -> (Jobs, Vec<Arc<Lock>>) {
35 let random = &environment.random;
36 let logger = &environment.logger;
37
38 let (mut jobs, locks) = read_required_jobs(api_problem, props, coord_index, job_index, random);
39 let conditional_jobs = read_conditional_jobs(api_problem, coord_index, job_index);
40
41 jobs.extend(conditional_jobs);
42
43 (Jobs::new(fleet, jobs, transport, logger).unwrap(), locks)
44}
45
46pub(super) fn read_locks(api_problem: &ApiProblem, job_index: &JobIndex) -> Vec<Arc<Lock>> {
47 if api_problem.plan.relations.as_ref().map_or(true, |r| r.is_empty()) {
48 return vec![];
49 }
50
51 let relations: HashMap<_, Vec<_>> =
52 api_problem.plan.relations.as_ref().unwrap().iter().fold(HashMap::new(), |mut acc, r| {
53 let shift_index = r.shift_index.unwrap_or_default();
54 acc.entry((r.vehicle_id.clone(), shift_index)).or_default().push(r.clone());
55
56 acc
57 });
58
59 relations.into_iter().fold(vec![], |mut acc, ((vehicle_id, shift_index), rels)| {
60 let condition = create_condition(vehicle_id.clone(), shift_index);
61 let details = rels.iter().fold(vec![], |mut acc, rel| {
62 let order = match rel.type_field {
63 RelationType::Any => LockOrder::Any,
64 RelationType::Sequence => LockOrder::Sequence,
65 RelationType::Strict => LockOrder::Strict,
66 };
67
68 let position = match (rel.jobs.first().map(|s| s.as_str()), rel.jobs.last().map(|s| s.as_str())) {
69 (Some("departure"), Some("arrival")) => LockPosition::Fixed,
70 (Some("departure"), _) => LockPosition::Departure,
71 (_, Some("arrival")) => LockPosition::Arrival,
72 _ => LockPosition::Any,
73 };
74
75 let (_, jobs) = rel
76 .jobs
77 .iter()
78 .filter(|job| job.as_str() != "departure" && job.as_str() != "arrival")
79 .fold((HashMap::<String, _>::default(), vec![]), |(mut indexer, mut jobs), job| {
80 let job_id = match job.as_str() {
81 "break" | "reload" | "recharge" => {
82 let entry = indexer.entry(job.clone()).or_insert(1_usize);
83 let job_index = *entry;
84 *entry += 1;
85 format!("{vehicle_id}_{job}_{shift_index}_{job_index}")
86 }
87 _ => job.clone(),
88 };
89 let job =
90 job_index.get(&job_id).cloned().unwrap_or_else(|| panic!("cannot find job with id: '{job_id}"));
91
92 jobs.push(job);
93
94 (indexer, jobs)
95 });
96
97 acc.push(LockDetail::new(order, position, jobs));
98
99 acc
100 });
101
102 acc.push(Arc::new(Lock::new(condition, details, false)));
103
104 acc
105 })
106}
107
108fn read_required_jobs(
109 api_problem: &ApiProblem,
110 props: &ProblemProperties,
111 coord_index: &CoordIndex,
112 job_index: &mut JobIndex,
113 random: &Arc<dyn Random>,
114) -> (Vec<Job>, Vec<Arc<Lock>>) {
115 let mut jobs = vec![];
116 let has_multi_dimens = props.has_multi_dimen_capacity;
117
118 let get_single_from_task = |task: &JobTask, activity_type: &str, is_static_demand: bool| {
119 let absent = (empty(), empty());
120 let capacity = task.demand.clone().map_or_else(empty, MultiDimLoad::new);
121 let demand = if is_static_demand { (capacity, empty()) } else { (empty(), capacity) };
122
123 let demand = match activity_type {
124 "pickup" => Demand { pickup: demand, delivery: absent },
125 "delivery" => Demand { pickup: absent, delivery: demand },
126 "replacement" => Demand { pickup: demand, delivery: demand },
127 "service" => Demand { pickup: absent, delivery: absent },
128 _ => panic!("invalid activity type."),
129 };
130
131 let places = task
132 .places
133 .iter()
134 .map(|p| (Some(p.location.clone()), p.duration, parse_times(&p.times), p.tag.clone()))
135 .collect();
136
137 get_single_with_dimens(places, demand, &task.order, activity_type, has_multi_dimens, coord_index)
138 };
139
140 api_problem.plan.jobs.iter().for_each(|job| {
141 let pickups = job.pickups.as_ref().map_or(0, |p| p.len());
142 let deliveries = job.deliveries.as_ref().map_or(0, |p| p.len());
143 let is_static_demand = pickups == 0 || deliveries == 0;
144
145 let singles =
146 job.pickups
147 .iter()
148 .flat_map(|tasks| tasks.iter().map(|task| get_single_from_task(task, "pickup", is_static_demand)))
149 .chain(job.deliveries.iter().flat_map(|tasks| {
150 tasks.iter().map(|task| get_single_from_task(task, "delivery", is_static_demand))
151 }))
152 .chain(
153 job.replacements
154 .iter()
155 .flat_map(|tasks| tasks.iter().map(|task| get_single_from_task(task, "replacement", true))),
156 )
157 .chain(
158 job.services
159 .iter()
160 .flat_map(|tasks| tasks.iter().map(|task| get_single_from_task(task, "service", false))),
161 )
162 .collect::<Vec<_>>();
163
164 assert!(!singles.is_empty());
165
166 let problem_job = if singles.len() > 1 {
167 let deliveries_start_index = job.pickups.as_ref().map_or(0, |p| p.len());
168 get_multi_job(job, singles, deliveries_start_index, random)
169 } else {
170 get_single_job(job, singles.into_iter().next().unwrap())
171 };
172
173 job_index.insert(job.id.clone(), problem_job.clone());
174 jobs.push(problem_job);
175 });
176
177 (jobs, vec![])
178}
179
180fn read_conditional_jobs(api_problem: &ApiProblem, coord_index: &CoordIndex, job_index: &mut JobIndex) -> Vec<Job> {
181 let mut jobs = vec![];
182
183 api_problem.fleet.vehicles.iter().for_each(|vehicle| {
184 for (shift_index, shift) in vehicle.shifts.iter().enumerate() {
185 if let Some(breaks) = &shift.breaks {
186 read_optional_breaks(coord_index, job_index, &mut jobs, vehicle, shift_index, breaks);
187 }
188
189 if let Some(reloads) = &shift.reloads {
190 read_reloads(coord_index, job_index, &mut jobs, vehicle, shift_index, reloads);
191 }
192
193 if let Some(recharges) = &shift.recharges {
194 read_recharges(coord_index, job_index, &mut jobs, vehicle, shift_index, recharges);
195 }
196 }
197 });
198
199 jobs
200}
201
202fn read_optional_breaks(
203 coord_index: &CoordIndex,
204 job_index: &mut JobIndex,
205 jobs: &mut Vec<Job>,
206 vehicle: &VehicleType,
207 shift_index: usize,
208 breaks: &[VehicleBreak],
209) {
210 (1..)
211 .zip(breaks.iter().filter_map(|vehicle_break| match vehicle_break {
212 VehicleBreak::Optional { time, places, policy } => Some((time, places, policy)),
213 VehicleBreak::Required { .. } => None,
214 }))
215 .flat_map(|(break_idx, (break_time, break_places, policy))| {
216 vehicle
217 .vehicle_ids
218 .iter()
219 .map(|vehicle_id| {
220 let times = match &break_time {
221 VehicleOptionalBreakTime::TimeWindow(time) if time.len() != 2 => {
222 panic!("break with invalid time window specified: must have start and end!")
223 }
224 VehicleOptionalBreakTime::TimeOffset(offsets) if offsets.len() != 2 => {
225 panic!("break with invalid offset specified: must have start and end!")
226 }
227 VehicleOptionalBreakTime::TimeWindow(time) => vec![TimeSpan::Window(parse_time_window(time))],
228 VehicleOptionalBreakTime::TimeOffset(offset) => {
229 vec![TimeSpan::Offset(TimeOffset::new(*offset.first().unwrap(), *offset.last().unwrap()))]
230 }
231 };
232
233 let job_id = format!("{vehicle_id}_break_{shift_index}_{break_idx}");
234 let places = break_places
235 .iter()
236 .map(|place| (place.location.clone(), place.duration, times.clone(), place.tag.clone()))
237 .collect();
238
239 let mut job =
240 get_conditional_job(coord_index, vehicle_id.clone(), &job_id, "break", shift_index, places);
241
242 if let Some(policy) = policy {
243 let policy = match policy {
244 VehicleOptionalBreakPolicy::SkipIfNoIntersection => BreakPolicy::SkipIfNoIntersection,
245 VehicleOptionalBreakPolicy::SkipIfArrivalBeforeEnd => BreakPolicy::SkipIfArrivalBeforeEnd,
246 };
247
248 job.dimens.set_break_policy(policy);
249 }
250
251 (job_id, job)
252 })
253 .collect::<Vec<_>>()
254 })
255 .for_each(|(job_id, single)| add_conditional_job(job_index, jobs, job_id, single));
256}
257
258fn read_reloads(
259 coord_index: &CoordIndex,
260 job_index: &mut JobIndex,
261 jobs: &mut Vec<Job>,
262 vehicle: &VehicleType,
263 shift_index: usize,
264 reloads: &[VehicleReload],
265) {
266 read_specific_job_places(
267 "reload",
268 coord_index,
269 job_index,
270 jobs,
271 vehicle,
272 shift_index,
273 reloads.iter().map(|reload| JobPlace {
274 location: reload.location.clone(),
275 duration: reload.duration,
276 times: reload.times.clone(),
277 tag: reload.tag.clone(),
278 }),
279 )
280}
281
282fn read_recharges(
283 coord_index: &CoordIndex,
284 job_index: &mut JobIndex,
285 jobs: &mut Vec<Job>,
286 vehicle: &VehicleType,
287 shift_index: usize,
288 recharges: &VehicleRecharges,
289) {
290 read_specific_job_places(
291 "recharge",
292 coord_index,
293 job_index,
294 jobs,
295 vehicle,
296 shift_index,
297 recharges.stations.iter().cloned(),
298 )
299}
300
301fn read_specific_job_places(
302 job_type: &str,
303 coord_index: &CoordIndex,
304 job_index: &mut JobIndex,
305 jobs: &mut Vec<Job>,
306 vehicle: &VehicleType,
307 shift_index: usize,
308 get_places: impl Iterator<Item = JobPlace>,
309) {
310 (1..)
311 .zip(get_places)
312 .flat_map(|(place_idx, place)| {
313 vehicle
314 .vehicle_ids
315 .iter()
316 .map(|vehicle_id| {
317 let job_id = format!("{vehicle_id}_{job_type}_{shift_index}_{place_idx}");
318 let times = parse_times(&place.times);
319
320 let job = get_conditional_job(
321 coord_index,
322 vehicle_id.clone(),
323 &job_id,
324 job_type,
325 shift_index,
326 vec![(Some(place.location.clone()), place.duration, times, place.tag.clone())],
327 );
328
329 (job_id, job)
330 })
331 .collect::<Vec<_>>()
332 })
333 .for_each(|(job_id, single)| add_conditional_job(job_index, jobs, job_id, single));
334}
335
336fn get_conditional_job(
337 coord_index: &CoordIndex,
338 vehicle_id: String,
339 job_id: &str,
340 job_type: &str,
341 shift_index: usize,
342 places: Vec<PlaceData>,
343) -> Single {
344 let mut single = get_single(places, coord_index);
345 single
346 .dimens
347 .set_job_id(job_id.to_string())
348 .set_job_type(job_type.to_string())
349 .set_shift_index(shift_index)
350 .set_vehicle_id(vehicle_id);
351
352 single
353}
354
355fn add_conditional_job(job_index: &mut JobIndex, jobs: &mut Vec<Job>, job_id: String, single: Single) {
356 let job = Job::Single(Arc::new(single));
357 job_index.insert(job_id, job.clone());
358 jobs.push(job);
359}
360
361fn get_single(places: Vec<PlaceData>, coord_index: &CoordIndex) -> Single {
362 let tags = places
363 .iter()
364 .map(|(_, _, _, tag)| tag)
365 .enumerate()
366 .filter_map(|(idx, tag)| tag.as_ref().map(|tag| (idx, tag.clone())))
367 .collect::<Vec<_>>();
368
369 let places = places
370 .into_iter()
371 .map(|(location, duration, times, _)| Place {
372 location: location.as_ref().and_then(|l| coord_index.get_by_loc(l)),
373 duration,
374 times,
375 })
376 .collect();
377
378 let mut dimens = Dimensions::default();
379
380 dimens.set_place_tags(tags);
381
382 Single { places, dimens }
383}
384
385fn get_single_with_dimens(
386 places: Vec<PlaceData>,
387 demand: Demand<MultiDimLoad>,
388 order: &Option<i32>,
389 activity_type: &str,
390 has_multi_dimens: bool,
391 coord_index: &CoordIndex,
392) -> Single {
393 let mut single = get_single(places, coord_index);
394 let dimens = &mut single.dimens;
395
396 if has_multi_dimens {
397 dimens.set_job_demand(demand)
398 } else {
399 dimens.set_job_demand(Demand {
400 pickup: (SingleDimLoad::new(demand.pickup.0.load[0]), SingleDimLoad::new(demand.pickup.1.load[0])),
401 delivery: (SingleDimLoad::new(demand.delivery.0.load[0]), SingleDimLoad::new(demand.delivery.1.load[0])),
402 })
403 }
404 .set_job_type(activity_type.to_string());
405
406 if let Some(order) = order {
407 dimens.set_job_order(*order);
408 }
409
410 single
411}
412
413fn fill_dimens(job: &ApiJob, dimens: &mut Dimensions) {
414 dimens.set_job_id(job.id.clone());
415
416 if let Some(value) = job.value {
417 dimens.set_job_value(value);
418 }
419
420 if let Some(group) = job.group.clone() {
421 dimens.set_job_group(group);
422 }
423
424 if let Some(compat) = job.compatibility.clone() {
425 dimens.set_job_compatibility(compat);
426 }
427
428 if let Some(skills) = get_skills(&job.skills) {
429 dimens.set_job_skills(skills);
430 }
431}
432
433fn get_single_job(job: &ApiJob, single: Single) -> Job {
434 let mut single = single;
435 fill_dimens(job, &mut single.dimens);
436
437 Job::Single(Arc::new(single))
438}
439
440fn get_multi_job(job: &ApiJob, singles: Vec<Single>, deliveries_start_index: usize, random: &Arc<dyn Random>) -> Job {
441 let mut dimens: Dimensions = Default::default();
442 fill_dimens(job, &mut dimens);
443
444 let singles = singles.into_iter().map(Arc::new).collect::<Vec<_>>();
445
446 let multi = if singles.len() == 2 && deliveries_start_index == 1 {
447 Multi::new_shared(singles, dimens)
448 } else {
449 let jobs_len = singles.len();
450 Multi::new_shared_with_permutator(
451 singles,
452 dimens,
453 Box::new(VariableJobPermutation::new(
454 jobs_len,
455 deliveries_start_index,
456 MULTI_JOB_SAMPLE_SIZE,
457 random.clone(),
458 )),
459 )
460 };
461
462 Job::Multi(multi)
463}
464
465fn create_condition(vehicle_id: String, shift_index: usize) -> Arc<dyn Fn(&Actor) -> bool + Sync + Send> {
466 Arc::new(move |actor: &Actor| {
467 *actor.vehicle.dimens.get_vehicle_id().unwrap() == vehicle_id
468 && actor.vehicle.dimens.get_shift_index().copied().unwrap() == shift_index
469 })
470}
471
472fn get_skills(skills: &Option<ApiJobSkills>) -> Option<FeatureJobSkills> {
473 skills
474 .as_ref()
475 .map(|skills| FeatureJobSkills::new(skills.all_of.clone(), skills.one_of.clone(), skills.none_of.clone()))
476}
477
478fn empty() -> MultiDimLoad {
479 MultiDimLoad::default()
480}
481
482fn parse_times(times: &Option<Vec<Vec<String>>>) -> Vec<TimeSpan> {
483 times.as_ref().map_or(vec![TimeSpan::Window(TimeWindow::max())], |tws| {
484 tws.iter().map(|tw| TimeSpan::Window(parse_time_window(tw))).collect()
485 })
486}