1#[cfg(test)]
2#[path = "../../../tests/unit/format/solution/geo_serializer_test.rs"]
3mod geo_serializer_test;
4
5use super::Solution;
6use crate::format::solution::{Activity, PointStop, Tour, UnassignedJob};
7use crate::format::{get_indices, CoordIndex, CustomLocationType, Location};
8use serde::{Deserialize, Serialize};
9use std::cmp::Ordering;
10use std::collections::BTreeMap;
11use std::io::{BufWriter, Error, ErrorKind, Write};
12use vrp_core::models::problem::Job;
13use vrp_core::prelude::*;
14
15#[derive(Clone, Debug, Deserialize, Serialize)]
17#[serde(tag = "type")]
18pub enum Geometry {
19 Point {
21 coordinates: (f64, f64),
23 },
24 LineString {
26 coordinates: Vec<(f64, f64)>,
28 },
29}
30
31#[derive(Clone, Debug, Deserialize, Serialize)]
33#[serde(tag = "type")]
34pub struct Feature {
35 pub properties: BTreeMap<String, String>,
37 pub geometry: Geometry,
39}
40
41#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
43#[serde(tag = "type")]
44pub struct FeatureCollection {
45 pub features: Vec<Feature>,
47}
48
49impl Eq for Geometry {}
50
51impl PartialEq for Geometry {
52 fn eq(&self, other: &Self) -> bool {
53 let compare_pair = |l_coord: &(f64, f64), r_coord: &(f64, f64)| {
54 l_coord.0.partial_cmp(&r_coord.0) == Some(Ordering::Equal)
55 && l_coord.1.partial_cmp(&r_coord.1) == Some(Ordering::Equal)
56 };
57
58 match (self, other) {
59 (Geometry::Point { coordinates: l_coord }, Geometry::Point { coordinates: r_coord }) => {
60 compare_pair(l_coord, r_coord)
61 }
62 (Geometry::LineString { coordinates: l_coords }, Geometry::LineString { coordinates: r_coords }) => {
63 l_coords.len() == r_coords.len()
64 && l_coords.iter().zip(r_coords.iter()).all(|(l_coord, r_coord)| compare_pair(l_coord, r_coord))
65 }
66 _ => false,
67 }
68 }
69}
70
71impl Eq for Feature {}
72
73impl PartialEq for Feature {
74 fn eq(&self, other: &Self) -> bool {
75 let same_properties = self.properties.len() == other.properties.len()
76 && self.properties.keys().all(|key| {
77 if let Some(value) = other.properties.get(key) {
78 self.properties[key] == *value
79 } else {
80 false
81 }
82 });
83
84 same_properties && self.geometry.eq(&other.geometry)
85 }
86}
87
88pub fn serialize_solution_as_geojson<W: Write>(
90 problem: &Problem,
91 solution: &Solution,
92 writer: &mut BufWriter<W>,
93) -> Result<(), Error> {
94 let geo_json = create_feature_collection(problem, solution)?;
95
96 serde_json::to_writer_pretty(writer, &geo_json).map_err(Error::from)
97}
98
99pub fn serialize_named_locations_as_geojson<W: Write>(
101 locations: &[(String, Location, usize)],
102 writer: &mut BufWriter<W>,
103) -> Result<(), Error> {
104 let geo_json = create_geojson_named_locations(locations)?;
105
106 serde_json::to_writer_pretty(writer, &geo_json).map_err(Error::from)
107}
108
109fn create_geojson_named_locations(locations: &[(String, Location, usize)]) -> Result<FeatureCollection, Error> {
110 let colors = get_more_colors();
111
112 Ok(FeatureCollection {
113 features: locations
114 .iter()
115 .map(|(name, location, index)| {
116 Ok(Feature {
117 properties: slice_to_map(&[
118 ("marker-color", colors[*index % colors.len()]),
119 ("marker-size", "medium"),
120 ("marker-symbol", "marker"),
121 ("name", name),
122 ]),
123 geometry: Geometry::Point { coordinates: get_lng_lat(location)? },
124 })
125 })
126 .collect::<Result<Vec<_>, Error>>()?,
127 })
128}
129
130fn slice_to_map(vec: &[(&str, &str)]) -> BTreeMap<String, String> {
131 vec.iter().map(|&(key, value)| (key.to_string(), value.to_string())).collect()
132}
133
134fn get_marker_symbol(stop: &PointStop) -> String {
135 let contains_activity_type =
136 |activity_type: &&str| stop.activities.iter().any(|activity| activity.activity_type == *activity_type);
137
138 if ["departure", "reload", "arrival"].iter().any(contains_activity_type) {
139 return "warehouse".to_string();
140 }
141
142 if contains_activity_type(&"recharge") {
143 return "charging-station".to_string();
144 }
145
146 if contains_activity_type(&"break") {
147 return "beer".to_string();
148 }
149
150 "marker".to_string()
151}
152
153fn get_stop_point(tour_idx: usize, stop_idx: usize, stop: &PointStop, color: &str) -> Result<Feature, Error> {
154 Ok(Feature {
156 properties: slice_to_map(&[
157 ("marker-color", color),
158 ("marker-size", "medium"),
159 ("marker-symbol", get_marker_symbol(stop).as_str()),
160 ("tour_idx", tour_idx.to_string().as_str()),
161 ("stop_idx", stop_idx.to_string().as_str()),
162 ("arrival", stop.time.arrival.as_str()),
163 ("departure", stop.time.departure.as_str()),
164 ("distance", stop.distance.to_string().as_str()),
165 ("jobs_ids", stop.activities.iter().map(|a| a.job_id.clone()).collect::<Vec<_>>().join(",").as_str()),
166 ]),
167 geometry: Geometry::Point { coordinates: get_lng_lat(&stop.location)? },
168 })
169}
170
171fn get_activity_point(
172 tour_idx: usize,
173 stop_idx: usize,
174 activity_idx: usize,
175 activity: &Activity,
176 location: &Location,
177 color: &str,
178) -> Result<Feature, Error> {
179 let time =
180 activity.time.as_ref().ok_or_else(|| Error::new(ErrorKind::InvalidData, "activity has no time defined"))?;
181
182 Ok(Feature {
183 properties: slice_to_map(&[
184 ("marker-color", color),
185 ("marker-size", "medium"),
186 ("marker-symbol", "marker"),
187 ("tour_idx", tour_idx.to_string().as_str()),
188 ("stop_idx", stop_idx.to_string().as_str()),
189 ("activity_idx", activity_idx.to_string().as_str()),
190 ("start", time.start.as_str()),
191 ("end", time.end.as_str()),
192 ("jobs_id", activity.job_id.as_str()),
193 ]),
194 geometry: Geometry::Point { coordinates: get_lng_lat(location)? },
195 })
196}
197
198fn get_cluster_geometry(tour_idx: usize, stop_idx: usize, stop: &PointStop) -> Result<Vec<Feature>, Error> {
199 let features = stop.activities.iter().enumerate().try_fold::<_, _, Result<_, Error>>(
200 Vec::<Feature>::new(),
201 |mut features, (activity_idx, activity)| {
202 let location = activity.location.clone().ok_or_else(|| invalid_data("activity without location"))?;
203 features.push(get_activity_point(
204 tour_idx,
205 stop_idx,
206 activity_idx,
207 activity,
208 &location,
209 get_color(tour_idx).as_str(),
210 )?);
211
212 let line_color = get_color_inverse(tour_idx);
213 let get_line = |from: (f64, f64), to: (f64, f64)| -> Feature {
214 Feature {
215 properties: slice_to_map(&[("stroke-width", "3"), ("stroke", line_color.as_str())]),
216 geometry: Geometry::LineString { coordinates: vec![from, to] },
217 }
218 };
219
220 if let Some(commute) = &activity.commute {
221 if let Some(forward) = &commute.forward {
222 features.push(get_line(get_lng_lat(&forward.location)?, get_lng_lat(&location)?));
223 }
224
225 if let Some(backward) = &commute.backward {
226 features.push(get_line(get_lng_lat(&location)?, get_lng_lat(&backward.location)?));
227 }
228 }
229
230 Ok(features)
231 },
232 )?;
233
234 Ok(features)
235}
236
237fn get_unassigned_points(
238 coord_index: &CoordIndex,
239 unassigned: &UnassignedJob,
240 job: &Job,
241 color: &str,
242) -> Result<Vec<Feature>, Error> {
243 job.places()
244 .filter_map(|place| place.location.and_then(|l| coord_index.get_by_idx(l)))
245 .map(|location| {
246 let coordinates = get_lng_lat(&location)?;
247 Ok(Feature {
248 properties: slice_to_map(&[
249 ("marker-color", color),
250 ("marker-size", "medium"),
251 ("marker-symbol", "roadblock"),
252 ("job_id", unassigned.job_id.as_str()),
253 (
254 "reasons",
255 unassigned
256 .reasons
257 .iter()
258 .map(|reason| format!("{}:{}", reason.code, reason.description))
259 .collect::<Vec<_>>()
260 .join(",")
261 .as_str(),
262 ),
263 ]),
264 geometry: Geometry::Point { coordinates },
265 })
266 })
267 .collect()
268}
269
270fn get_tour_line(tour_idx: usize, tour: &Tour, color: &str) -> Result<Feature, Error> {
271 let stops = tour.stops.iter().filter_map(|stop| stop.as_point()).collect::<Vec<_>>();
272
273 let coordinates = stops.iter().map(|stop| get_lng_lat(&stop.location)).collect::<Result<_, Error>>()?;
274
275 Ok(Feature {
276 properties: slice_to_map(&[
277 ("vehicle_id", tour.vehicle_id.as_str()),
278 ("tour_idx", tour_idx.to_string().as_str()),
279 ("shift_idx", tour.shift_index.to_string().as_str()),
280 ("activities", stops.iter().map(|stop| stop.activities.len()).sum::<usize>().to_string().as_str()),
281 ("distance", (stops.last().unwrap().distance).to_string().as_str()),
282 ("departure", stops.first().unwrap().time.departure.as_str()),
283 ("arrival", stops.last().unwrap().time.arrival.as_str()),
284 ("stroke-width", "4"),
285 ("stroke", color),
286 ]),
287 geometry: Geometry::LineString { coordinates },
288 })
289}
290
291pub(crate) fn create_feature_collection(problem: &Problem, solution: &Solution) -> Result<FeatureCollection, Error> {
293 let stop_markers = solution
294 .tours
295 .iter()
296 .enumerate()
297 .flat_map(|(tour_idx, tour)| {
298 tour.stops
299 .iter()
300 .enumerate()
301 .filter_map(|(stop_idx, stop)| stop.as_point().map(|stop| (stop_idx, stop)))
302 .map(move |(stop_idx, stop)| {
303 get_stop_point(tour_idx, stop_idx, stop, get_color_inverse(tour_idx).as_str())
304 })
305 })
306 .collect::<Result<Vec<_>, _>>()?;
307
308 let clusters_geometry = solution
309 .tours
310 .iter()
311 .enumerate()
312 .flat_map(|(tour_idx, tour)| {
313 tour.stops
314 .iter()
315 .enumerate()
316 .filter_map(|(stop_idx, stop)| stop.as_point().map(|stop| (stop_idx, stop)))
317 .filter(|(_, stop)| stop.parking.is_some())
318 .map(move |(stop_idx, stop)| get_cluster_geometry(tour_idx, stop_idx, stop))
319 })
320 .collect::<Result<Vec<_>, _>>()?
321 .into_iter()
322 .flatten();
323
324 let stop_lines = solution
325 .tours
326 .iter()
327 .enumerate()
328 .map(|(tour_idx, tour)| get_tour_line(tour_idx, tour, get_color(tour_idx).as_str()))
329 .collect::<Result<Vec<_>, _>>()?;
330
331 let (job_index, coord_index) = get_indices(&problem.extras).map_err(|err| Error::new(ErrorKind::Other, err))?;
332
333 let unassigned_markers = solution
334 .unassigned
335 .iter()
336 .flat_map(|unassigned| unassigned.iter())
337 .enumerate()
338 .map(|(idx, unassigned_job)| {
339 let job = job_index
340 .get(&unassigned_job.job_id)
341 .ok_or_else(|| invalid_data(format!("cannot find job: {}", unassigned_job.job_id).as_str()))?;
342 let color = get_color(idx);
343 get_unassigned_points(coord_index.as_ref(), unassigned_job, job, color.as_str())
344 })
345 .collect::<Result<Vec<Vec<Feature>>, Error>>()?
346 .into_iter()
347 .flatten();
348
349 Ok(FeatureCollection {
350 features: stop_markers
351 .into_iter()
352 .chain(stop_lines)
353 .chain(unassigned_markers)
354 .chain(clusters_geometry)
355 .collect(),
356 })
357}
358
359fn get_color(idx: usize) -> String {
360 static COLOR_LIST: ColorList = get_color_list();
361
362 let idx = idx % COLOR_LIST.len();
363
364 (**COLOR_LIST.get(idx).as_ref().unwrap()).to_string()
365}
366
367fn get_color_inverse(idx: usize) -> String {
368 static COLOR_LIST: ColorList = get_color_list();
369
370 let idx = (COLOR_LIST.len() - idx + 1) % COLOR_LIST.len();
371
372 (**COLOR_LIST.get(idx).as_ref().unwrap()).to_string()
373}
374
375fn get_lng_lat(location: &Location) -> Result<(f64, f64), Error> {
376 match location {
377 Location::Coordinate { lat, lng } => Ok((*lng, *lat)),
378 Location::Reference { index: _ } => {
379 Err(Error::new(ErrorKind::InvalidData, "geojson cannot be used with location indices"))
380 }
381 Location::Custom { r#type: CustomLocationType::Unknown } => {
382 Err(Error::new(ErrorKind::InvalidData, "geojson cannot be used with location unknown type"))
383 }
384 }
385}
386
387fn invalid_data(msg: &str) -> Error {
388 Error::new(ErrorKind::InvalidData, msg)
389}
390
391type ColorList = &'static [&'static str; 15];
392
393const fn get_color_list() -> ColorList {
395 &[
396 "#e6194b", "#3cb44b", "#4363d8", "#f58231", "#911eb4", "#46f0f0", "#f032e6", "#bcf60c", "#008080", "#e6beff",
397 "#9a6324", "#800000", "#808000", "#000075", "#808080",
398 ]
399}
400
401type MoreColorList = &'static [&'static str; 128];
402
403const fn get_more_colors() -> MoreColorList {
405 &[
406 "#000000", "#FFFF00", "#1CE6FF", "#FF34FF", "#FF4A46", "#008941", "#006FA6", "#A30059", "#FFDBE5", "#7A4900",
407 "#0000A6", "#63FFAC", "#B79762", "#004D43", "#8FB0FF", "#997D87", "#5A0007", "#809693", "#FEFFE6", "#1B4400",
408 "#4FC601", "#3B5DFF", "#4A3B53", "#FF2F80", "#61615A", "#BA0900", "#6B7900", "#00C2A0", "#FFAA92", "#FF90C9",
409 "#B903AA", "#D16100", "#DDEFFF", "#000035", "#7B4F4B", "#A1C299", "#300018", "#0AA6D8", "#013349", "#00846F",
410 "#372101", "#FFB500", "#C2FFED", "#A079BF", "#CC0744", "#C0B9B2", "#C2FF99", "#001E09", "#00489C", "#6F0062",
411 "#0CBD66", "#EEC3FF", "#456D75", "#B77B68", "#7A87A1", "#788D66", "#885578", "#FAD09F", "#FF8A9A", "#D157A0",
412 "#BEC459", "#456648", "#0086ED", "#886F4C", "#34362D", "#B4A8BD", "#00A6AA", "#452C2C", "#636375", "#A3C8C9",
413 "#FF913F", "#938A81", "#575329", "#00FECF", "#B05B6F", "#8CD0FF", "#3B9700", "#04F757", "#C8A1A1", "#1E6E00",
414 "#7900D7", "#A77500", "#6367A9", "#A05837", "#6B002C", "#772600", "#D790FF", "#9B9700", "#549E79", "#FFF69F",
415 "#201625", "#72418F", "#BC23FF", "#99ADC0", "#3A2465", "#922329", "#5B4534", "#FDE8DC", "#404E55", "#0089A3",
416 "#CB7E98", "#A4E804", "#324E72", "#6A3A4C", "#83AB58", "#001C1E", "#D1F7CE", "#004B28", "#C8D0F6", "#A3A489",
417 "#806C66", "#222800", "#BF5650", "#E83000", "#66796D", "#DA007C", "#FF1A59", "#8ADBB4", "#1E0200", "#5B4E51",
418 "#C895C5", "#320033", "#FF6832", "#66E1D3", "#CFCDAC", "#D0AC94", "#7ED379", "#012C58",
419 ]
420}