Skip to main content

paiagram_core/export/
oudia.rs

1use std::borrow::Cow;
2
3use bevy::ecs::entity::EntityHashMap;
4use bevy::prelude::*;
5use either::Either;
6use encoding_rs::SHIFT_JIS;
7use paiagram_oudia::{SerializeToOud, Structure, pair, structure};
8use smallvec::{SmallVec, smallvec};
9
10use crate::class::ClassQuery;
11use crate::entry::{EntryQuery, EntryQueryItem, TravelMode};
12use crate::route::{Route, RouteByDirectionTrips};
13use crate::station::{ParentStationOrStation, Station};
14use crate::trip::{TripQuery, TripQueryItem};
15
16fn make_disp_prop() -> Structure<'static> {
17    structure!("DispProp" =>
18        pair!("JikokuhyouFont"        => "PointTextHeight=9;Facename=MS ゴシック"),
19        pair!("JikokuhyouFont"        => "PointTextHeight=9;Facename=MS ゴシック;Bold=1"),
20        pair!("JikokuhyouFont"        => "PointTextHeight=9;Facename=MS ゴシック;Itaric=1"),
21        pair!("JikokuhyouFont"        => "PointTextHeight=9;Facename=MS ゴシック;Bold=1;Itaric=1"),
22        pair!("JikokuhyouFont"        => "PointTextHeight=9;Facename=MS ゴシック"),
23        pair!("JikokuhyouFont"        => "PointTextHeight=9;Facename=MS ゴシック"),
24        pair!("JikokuhyouFont"        => "PointTextHeight=9;Facename=MS ゴシック"),
25        pair!("JikokuhyouFont"        => "PointTextHeight=9;Facename=MS ゴシック"),
26        pair!("JikokuhyouVFont"       => "PointTextHeight=9;Facename=@MS ゴシック"),
27        pair!("DiaEkimeiFont"         => "PointTextHeight=9;Facename=MS ゴシック"),
28        pair!("DiaJikokuFont"         => "PointTextHeight=9;Facename=MS ゴシック"),
29        pair!("DiaRessyaFont"         => "PointTextHeight=9;Facename=MS ゴシック"),
30        pair!("CommentFont"           => "PointTextHeight=9;Facename=MS ゴシック"),
31        pair!("DiaMojiColor"          => "00000000"),
32        pair!("DiaHaikeiColor"        => "00FFFFFF"),
33        pair!("DiaRessyaColor"        => "00000000"),
34        pair!("DiaJikuColor"          => "00C0C0C0"),
35        pair!("EkimeiLength"          => "6"),
36        pair!("JikokuhyouRessyaWidth" => "5"),
37    )
38}
39
40pub struct OuDia<'a> {
41    pub route_entity: Entity,
42    pub world: &'a mut World,
43}
44
45impl<'a> super::ExportObject for OuDia<'a> {
46    fn extension(&self) -> impl AsRef<str> {
47        ".oud"
48    }
49    fn export_to_buffer(&mut self, buffer: &mut Vec<u8>) {
50        let mut route_buf = vec![pair!(
51            "Rosenmei" =>
52            self.world
53                .get::<Name>(self.route_entity)
54                .unwrap()
55                .to_string()
56        )];
57        self.world
58            .run_system_cached_with(make_stations, (self.route_entity, &mut route_buf))
59            .unwrap();
60        let class_map = self
61            .world
62            .run_system_cached_with(make_classes, &mut route_buf)
63            .unwrap();
64        self.world
65            .run_system_cached_with(
66                make_diagram,
67                (&mut route_buf, self.route_entity, &class_map),
68            )
69            .unwrap();
70        route_buf.extend_from_slice(&[
71            pair!("KitenJikoku" => "200"),
72            pair!("DiagramDgrYZahyouKyoriDefault" => "60"),
73            pair!("Comment" => concat!("Exported by Paiagram ", env!("CARGO_PKG_VERSION"))),
74        ]);
75        let root = vec![
76            pair!("FileType" => "OuDia.1.02"),
77            structure!("Rosen" => ..route_buf),
78            make_disp_prop(),
79            pair!("FileTypeAppComment" =>
80                concat!("Exported by Paiagram ", env!("CARGO_PKG_VERSION")),
81            ),
82        ];
83        let mut utf8_buf = Vec::new();
84        root.serialize_oud_to(&mut utf8_buf).unwrap();
85        let s = String::from_utf8(utf8_buf).unwrap();
86        // extra step: convert the buffer to shift-jis
87        let (cow, _, _) = SHIFT_JIS.encode(s.as_str());
88        *buffer = cow.into_owned();
89    }
90}
91
92fn split_first_middle_last<T>(slice: &[T]) -> Option<(&T, &[T], &T)> {
93    let (first, rest) = slice.split_first()?;
94    let (last, middle) = rest.split_last().map_or((first, &[][..]), |(l, m)| (l, m));
95    Some((first, middle, last))
96}
97
98fn make_stations(
99    (In(route_entity), InMut(buf)): (In<Entity>, InMut<Vec<Structure<'static>>>),
100    route_q: Query<&Route>,
101    station_name_q: Query<&Name, With<Station>>,
102) {
103    let route = route_q.get(route_entity).unwrap();
104    let Some((first, rest, last)) = split_first_middle_last(&route.stops) else {
105        return;
106    };
107    let make_station = |e: Entity, departure_display: &'static str| -> Structure<'static> {
108        let name = station_name_q.get(e).unwrap().to_string();
109        structure!("Eki" =>
110            pair!("Ekimei"           => name),              // 駅名
111            pair!("Ekijikokukeisiki" => departure_display), // 駅時刻形式
112            pair!("Ekikibo"          => "Ekikibo_Ippan"),   // 駅規模
113        )
114    };
115
116    let first_iter = std::iter::once(make_station(*first, "Jikokukeisiki_NoboriChaku"));
117    let mid_iter = rest
118        .iter()
119        .copied()
120        .map(|e| make_station(e, "Jikokukeisiki_Hatsu"));
121    let last_iter = std::iter::once(make_station(*last, "Jikokukeisiki_KudariChaku"));
122    buf.extend(first_iter);
123    buf.extend(mid_iter);
124    buf.extend(last_iter);
125}
126
127fn make_classes(
128    InMut(buf): InMut<Vec<Structure<'static>>>,
129    class_q: Query<ClassQuery>,
130) -> EntityHashMap<usize> {
131    let mut class_map = EntityHashMap::<usize>::new();
132    let iter = class_q.iter().map(|it| {
133        // ARGB
134        let len = class_map.len();
135        class_map.insert(it.entity, len);
136        let color = it.stroke.color.get(true);
137        let color_string = format!(
138            "00{:02X}{:02X}{:02X}",
139            // color.a(),
140            color.b(),
141            color.g(),
142            color.r(),
143        );
144        structure!("Ressyasyubetsu" =>
145            pair!("Syubetsumei"         => it.name.to_string()),
146            pair!("Ryakusyou"           => it.name.to_string()),
147            pair!("JikokuhyouMojiColor" => color_string.clone()),
148            pair!("JikokuhyouFontIndex" => "0"),
149            pair!("DiagramSenColor"     => color_string),
150            pair!("DiagramSenStyle"     => "SenStyle_Jissen"),
151            pair!("StopMarkDrawType"    => "EStopMarkDrawType_DrawOnStop"),
152        )
153    });
154    buf.extend(iter);
155    class_map
156}
157
158fn make_diagram(
159    (InMut(buf), In(route_entity), InRef(class_map)): (
160        InMut<Vec<Structure<'static>>>,
161        In<Entity>,
162        InRef<EntityHashMap<usize>>,
163    ),
164    route_q: Query<(&Route, &RouteByDirectionTrips)>,
165    entry_q: Query<EntryQuery>,
166    trip_q: Query<TripQuery>,
167    parent_station_or_station: Query<ParentStationOrStation>,
168) {
169    // downward: Nobori, Upward: Kudari
170    let (route, RouteByDirectionTrips { downward, upward }) = route_q.get(route_entity).unwrap();
171    let mut dia_buf = Vec::new();
172    dia_buf.push(pair!("DiaName" => "Paiagram Exported"));
173    dia_buf.push(make_trainset_by_direction(
174        true,
175        trip_q.iter_many(downward.as_slice()),
176        route.stops.as_slice(),
177        class_map,
178        &entry_q,
179        &parent_station_or_station,
180    ));
181    dia_buf.push(make_trainset_by_direction(
182        false,
183        trip_q.iter_many(upward.as_slice()),
184        route.stops.as_slice(),
185        class_map,
186        &entry_q,
187        &parent_station_or_station,
188    ));
189    buf.push(structure!("Dia" => ..dia_buf));
190}
191
192fn make_trainset_by_direction<'a>(
193    downwards: bool,
194    trips_iter: impl Iterator<Item = TripQueryItem<'a, 'a>>,
195    stops: &[Entity],
196    class_map: &EntityHashMap<usize>,
197    entry_q: &Query<EntryQuery>,
198    parent_station_or_station: &Query<ParentStationOrStation>,
199) -> Structure<'static> {
200    let format_time = |it: EntryQueryItem| -> String {
201        match (it.mode.arr, it.mode.dep) {
202            // arr at
203            (Some(TravelMode::At(at)), TravelMode::At(dt)) => {
204                let (ah, am, ..) = at.to_hmsd();
205                let (dh, dm, ..) = dt.to_hmsd();
206                format!("{};{}{:02}/{}{:02}", STOP, ah, am, dh, dm)
207            }
208            (Some(TravelMode::At(at)), TravelMode::For(d)) => {
209                let (ah, am, ..) = at.to_hmsd();
210                let (dh, dm, ..) = (at + d).to_hmsd();
211                format!("{};{}{:02}/{}{:02}", STOP, ah, am, dh, dm)
212            }
213            (Some(TravelMode::At(at)), TravelMode::Flexible) => {
214                let (ah, am, ..) = at.to_hmsd();
215                format!("{};{}{:02}/", STOP, ah, am)
216            }
217            // arr for
218            (Some(TravelMode::For(_)), TravelMode::At(dt)) => {
219                let (dh, dm, ..) = dt.to_hmsd();
220                let Some(e) = it.estimate else {
221                    return format!("{};{}{:02}", STOP, dh, dm);
222                };
223                let (ah, am, ..) = e.arr.to_hmsd();
224                format!("{};{}{:02}/{}{:02}", STOP, ah, am, dh, dm)
225            }
226            (Some(TravelMode::For(_)), TravelMode::For(_)) => {
227                let Some(e) = it.estimate else {
228                    return STOP.to_string();
229                };
230                let (ah, am, ..) = e.arr.to_hmsd();
231                let (dh, dm, ..) = e.dep.to_hmsd();
232                format!("{};{}{:02}/{}{:02}", STOP, ah, am, dh, dm)
233            }
234            (Some(TravelMode::For(_)), TravelMode::Flexible) => {
235                let Some(e) = it.estimate else {
236                    return STOP.to_string();
237                };
238                let (ah, am, ..) = e.arr.to_hmsd();
239                format!("{};{}{:02}/", STOP, ah, am)
240            }
241            // arr flexible
242            (Some(TravelMode::Flexible), TravelMode::At(t)) => {
243                let (dh, dm, ..) = t.to_hmsd();
244                format!("{};{}{:02}", STOP, dh, dm)
245            }
246            (Some(TravelMode::Flexible), TravelMode::For(_)) => {
247                let Some(e) = it.estimate else {
248                    return STOP.to_string();
249                };
250                let (ah, am, ..) = e.arr.to_hmsd();
251                let (dh, dm, ..) = e.dep.to_hmsd();
252                format!("{};{}{:02}/{}{:02}", STOP, ah, am, dh, dm)
253            }
254            (Some(TravelMode::Flexible), TravelMode::Flexible) => STOP.to_string(),
255            // arr none
256            (None, TravelMode::At(t)) => {
257                let (h, m, ..) = t.to_hmsd();
258                format!("{};{}{:02}", BYPASS, h, m)
259            }
260            // TODO: switch to if let guard
261            (None, TravelMode::For(_)) => {
262                let Some(e) = it.estimate else {
263                    return BYPASS.to_string();
264                };
265                let (h, m, ..) = e.dep.to_hmsd();
266                format!("{};{}{:02}", BYPASS, h, m)
267            }
268            (None, TravelMode::Flexible) => BYPASS.to_string(),
269        }
270    };
271    let magic_word = if downwards { "Kudari" } else { "Nobori" };
272    let mut trips = Vec::new();
273    const STOP: &str = "1";
274    const BYPASS: &str = "2";
275    const NO_OPERATION: &str = "";
276    for it in trips_iter {
277        let a = class_map.get(&it.class.entity());
278        let mut v: SmallVec<[Cow<'static, str>; 1]> = smallvec![NO_OPERATION.into(); stops.len()];
279        let schedule_it = entry_q.iter_many(it.schedule.iter());
280        let mut next_abs_idx = 0;
281        let mut stations = if downwards {
282            Either::Left(stops.iter())
283        } else {
284            Either::Right(stops.iter().rev())
285        };
286        for it in schedule_it {
287            let station_entity = parent_station_or_station.get(it.stop()).unwrap().parent();
288            // we reuse the same iterator here
289            // the pointer would advance every time we use the .position() method
290            if let Some(found_pos) = stations.position(|it| *it == station_entity) {
291                let abs_idx = next_abs_idx + found_pos;
292                v[abs_idx] = format_time(it).into();
293                next_abs_idx = abs_idx + 1;
294            }
295        }
296        trips.push(structure!("Ressya" =>
297            pair!("Houkou"       => magic_word),
298            pair!("Syubetsu"     => a.unwrap().to_string()),
299            pair!("Ressyabangou" => it.name.to_string()),
300            pair!("EkiJikoku"    => ..v)
301        ));
302    }
303    structure!(magic_word => ..trips)
304}