Skip to main content

vrp_pragmatic/format/solution/
geo_serializer.rs

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/// Represents geometry of the feature.
16#[derive(Clone, Debug, Deserialize, Serialize)]
17#[serde(tag = "type")]
18pub enum Geometry {
19    /// A point.
20    Point {
21        /// Point's longitude and latitude.
22        coordinates: (f64, f64),
23    },
24    /// A line string.
25    LineString {
26        /// List of longitude and latitude pairs.
27        coordinates: Vec<(f64, f64)>,
28    },
29}
30
31/// Represents geo json feature.
32#[derive(Clone, Debug, Deserialize, Serialize)]
33#[serde(tag = "type")]
34pub struct Feature {
35    /// Feature properties.
36    pub properties: BTreeMap<String, String>,
37    /// Feature geometry.
38    pub geometry: Geometry,
39}
40
41/// Represents a feature collection.
42#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
43#[serde(tag = "type")]
44pub struct FeatureCollection {
45    /// List of features.
46    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
88/// Serializes solution into geo json format.
89pub 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
99/// Serializes named location list with their color index.
100pub 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    // TODO add parking
155    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
291/// Creates solution as geo json.
292pub(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
393/// Returns list of human distinguishable colors.
394const 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
403/// Returns more colors.
404const 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}