1use serde::{Deserialize, Serialize};
2
3use geom::{Distance, LonLat, Pt2D, Ring};
4use map_gui::render::DrawOptions;
5use widgetry::mapspace::{ObjectID, World, WorldOutcome};
6use widgetry::tools::{ChooseSomething, Lasso, PromptInput};
7use widgetry::{
8 lctrl, Choice, Color, DrawBaselayer, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key,
9 Line, Outcome, Panel, SimpleState, State, Text, TextBox, VerticalAlignment, Widget,
10};
11
12use crate::app::{App, ShowEverything, Transition};
13
14pub struct StoryMapEditor {
19 panel: Panel,
20 story: StoryMap,
21 world: World<MarkerID>,
22
23 dirty: bool,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30struct MarkerID(usize);
31impl ObjectID for MarkerID {}
32
33impl StoryMapEditor {
34 pub fn new_state(ctx: &mut EventCtx) -> Box<dyn State<App>> {
35 Self::from_story(ctx, StoryMap::new())
36 }
37
38 fn from_story(ctx: &mut EventCtx, story: StoryMap) -> Box<dyn State<App>> {
39 let mut state = StoryMapEditor {
40 panel: Panel::empty(ctx),
41 story,
42 world: World::new(),
43
44 dirty: false,
45 };
46 state.rebuild_panel(ctx);
47 state.rebuild_world(ctx);
48 Box::new(state)
49 }
50
51 fn rebuild_panel(&mut self, ctx: &mut EventCtx) {
52 self.panel = Panel::new_builder(Widget::col(vec![
53 Widget::row(vec![
54 Line("Story map editor").small_heading().into_widget(ctx),
55 Widget::vert_separator(ctx, 30.0),
56 ctx.style()
57 .btn_outline
58 .popup(&self.story.name)
59 .hotkey(lctrl(Key::L))
60 .build_widget(ctx, "load"),
61 ctx.style()
62 .btn_plain
63 .icon("system/assets/tools/save.svg")
64 .hotkey(lctrl(Key::S))
65 .disabled(!self.dirty)
66 .build_widget(ctx, "save"),
67 ctx.style().btn_close_widget(ctx),
68 ]),
69 ctx.style()
70 .btn_plain
71 .icon_text("system/assets/tools/select.svg", "Draw freehand")
72 .hotkey(Key::F)
73 .build_def(ctx),
74 ]))
75 .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
76 .build(ctx);
77 }
78
79 fn rebuild_world(&mut self, ctx: &mut EventCtx) {
80 let mut world = World::new();
81
82 for (idx, marker) in self.story.markers.iter().enumerate() {
83 let mut draw_normal = GeomBatch::new();
84 let label_center = if marker.pts.len() == 1 {
85 draw_normal = map_gui::tools::goal_marker(ctx, marker.pts[0], 2.0);
87 marker.pts[0]
88 } else {
89 let poly = Ring::must_new(marker.pts.clone()).into_polygon();
90 draw_normal.push(Color::RED.alpha(0.8), poly.clone());
91 draw_normal.push(Color::RED, poly.to_outline(Distance::meters(1.0)));
92 poly.polylabel()
93 };
94
95 let mut draw_hovered = draw_normal.clone();
96
97 draw_normal.append(
98 Text::from(&marker.label)
99 .bg(Color::CYAN)
100 .render_autocropped(ctx)
101 .scale(0.5)
102 .centered_on(label_center),
103 );
104 let hitbox = draw_normal.get_bounds().to_circle().to_polygon();
105 draw_hovered.append(
106 Text::from(&marker.label)
107 .bg(Color::CYAN)
108 .render_autocropped(ctx)
109 .scale(0.75)
110 .centered_on(label_center),
111 );
112
113 world
114 .add(MarkerID(idx))
115 .hitbox(hitbox)
116 .draw(draw_normal)
117 .draw_hovered(draw_hovered)
118 .hotkey(Key::Backspace, "delete")
119 .clickable()
120 .draggable()
121 .build(ctx);
122 }
123
124 world.initialize_hover(ctx);
125 world.rebuilt_during_drag(ctx, &self.world);
126 self.world = world;
127 }
128}
129
130impl State<App> for StoryMapEditor {
131 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
132 match self.world.event(ctx) {
133 WorldOutcome::ClickedFreeSpace(pt) => {
134 self.story.markers.push(Marker {
135 pts: vec![pt],
136 label: String::new(),
137 });
138 self.dirty = true;
139 self.rebuild_panel(ctx);
140 self.rebuild_world(ctx);
141 return Transition::Push(EditingMarker::new_state(
142 ctx,
143 self.story.markers.len() - 1,
144 "new marker",
145 ));
146 }
147 WorldOutcome::Dragging {
148 obj: MarkerID(idx),
149 dx,
150 dy,
151 ..
152 } => {
153 for pt in &mut self.story.markers[idx].pts {
154 *pt = pt.offset(dx, dy);
155 }
156 self.dirty = true;
157 self.rebuild_panel(ctx);
158 self.rebuild_world(ctx);
159 }
160 WorldOutcome::Keypress("delete", MarkerID(idx)) => {
161 self.story.markers.remove(idx);
162 self.dirty = true;
163 self.rebuild_panel(ctx);
164 self.rebuild_world(ctx);
165 }
166 WorldOutcome::ClickedObject(MarkerID(idx)) => {
167 return Transition::Push(EditingMarker::new_state(
168 ctx,
169 idx,
170 &self.story.markers[idx].label,
171 ));
172 }
173 _ => {}
174 }
175
176 if let Outcome::Clicked(x) = self.panel.event(ctx) {
177 match x.as_ref() {
178 "close" => {
179 return Transition::Pop;
181 }
182 "save" => {
183 if self.story.name == "new story" {
184 return Transition::Push(PromptInput::new_state(
185 ctx,
186 "Name this story map",
187 String::new(),
188 Box::new(|name, _, _| {
189 Transition::Multi(vec![
190 Transition::Pop,
191 Transition::ModifyState(Box::new(move |state, ctx, app| {
192 let editor =
193 state.downcast_mut::<StoryMapEditor>().unwrap();
194 editor.story.name = name;
195 editor.story.save(app);
196 editor.dirty = false;
197 editor.rebuild_panel(ctx);
198 })),
199 ])
200 }),
201 ));
202 } else {
203 self.story.save(app);
204 self.dirty = false;
205 self.rebuild_panel(ctx);
206 }
207 }
208 "load" => {
209 let mut choices = Vec::new();
211 for (name, story) in
212 abstio::load_all_objects::<RecordedStoryMap>(abstio::path_player("stories"))
213 {
214 if story.name == self.story.name {
215 continue;
216 }
217 if let Some(s) = StoryMap::load(app, story) {
218 choices.push(Choice::new(name, s));
219 }
220 }
221 choices.push(Choice::new(
222 "new story",
223 StoryMap {
224 name: "new story".to_string(),
225 markers: Vec::new(),
226 },
227 ));
228
229 return Transition::Push(ChooseSomething::new_state(
230 ctx,
231 "Load story",
232 choices,
233 Box::new(|story, ctx, _| {
234 Transition::Multi(vec![
235 Transition::Pop,
236 Transition::Replace(StoryMapEditor::from_story(ctx, story)),
237 ])
238 }),
239 ));
240 }
241 "Draw freehand" => {
242 return Transition::Push(Box::new(DrawFreehand {
243 lasso: Lasso::new(Distance::meters(1.0)),
244 new_idx: self.story.markers.len(),
245 }));
246 }
247 _ => unreachable!(),
248 }
249 }
250
251 Transition::Keep
252 }
253
254 fn draw_baselayer(&self) -> DrawBaselayer {
255 DrawBaselayer::Custom
256 }
257
258 fn draw(&self, g: &mut GfxCtx, app: &App) {
259 let mut opts = DrawOptions::new();
260 opts.label_buildings = true;
261 app.draw(g, opts, &ShowEverything::new());
262
263 self.panel.draw(g);
264 self.world.draw(g);
265 }
266}
267
268#[derive(Clone, Serialize, Deserialize)]
269struct RecordedStoryMap {
270 name: String,
271 markers: Vec<(Vec<LonLat>, String)>,
272}
273
274struct StoryMap {
275 name: String,
276 markers: Vec<Marker>,
277}
278
279struct Marker {
280 pts: Vec<Pt2D>,
281 label: String,
282}
283
284impl StoryMap {
285 fn new() -> StoryMap {
286 StoryMap {
287 name: "new story".to_string(),
288 markers: Vec::new(),
289 }
290 }
291
292 fn load(app: &App, story: RecordedStoryMap) -> Option<StoryMap> {
293 let mut markers = Vec::new();
294 for (gps_pts, label) in story.markers {
295 markers.push(Marker {
296 pts: app.primary.map.get_gps_bounds().try_convert(&gps_pts)?,
297 label,
298 });
299 }
300 Some(StoryMap {
301 name: story.name,
302 markers,
303 })
304 }
305
306 fn save(&self, app: &App) {
307 let story = RecordedStoryMap {
308 name: self.name.clone(),
309 markers: self
310 .markers
311 .iter()
312 .map(|m| {
313 (
314 app.primary.map.get_gps_bounds().convert_back(&m.pts),
315 m.label.clone(),
316 )
317 })
318 .collect(),
319 };
320 abstio::write_json(
321 abstio::path_player(format!("stories/{}.json", story.name)),
322 &story,
323 );
324 }
325}
326
327struct EditingMarker {
328 idx: usize,
329}
330
331impl EditingMarker {
332 fn new_state(ctx: &mut EventCtx, idx: usize, label: &str) -> Box<dyn State<App>> {
333 let panel = Panel::new_builder(Widget::col(vec![
334 Widget::row(vec![
335 Line("Editing marker").small_heading().into_widget(ctx),
336 ctx.style().btn_close_widget(ctx),
337 ]),
338 ctx.style().btn_outline.text("delete").build_def(ctx),
339 TextBox::default_widget(ctx, "label", label.to_string()),
340 ctx.style()
341 .btn_outline
342 .text("confirm")
343 .hotkey(Key::Enter)
344 .build_def(ctx),
345 ]))
346 .build(ctx);
347 <dyn SimpleState<_>>::new_state(panel, Box::new(EditingMarker { idx }))
348 }
349}
350
351impl SimpleState<App> for EditingMarker {
352 fn on_click(
353 &mut self,
354 _: &mut EventCtx,
355 _: &mut App,
356 x: &str,
357 panel: &mut Panel,
358 ) -> Transition {
359 match x {
360 "close" => Transition::Pop,
361 "confirm" => {
362 let idx = self.idx;
363 let label = panel.text_box("label");
364 Transition::Multi(vec![
365 Transition::Pop,
366 Transition::ModifyState(Box::new(move |state, ctx, _| {
367 let editor = state.downcast_mut::<StoryMapEditor>().unwrap();
368 editor.story.markers[idx].label = label;
369
370 editor.dirty = true;
371 editor.rebuild_panel(ctx);
372 editor.rebuild_world(ctx);
373 })),
374 ])
375 }
376 "delete" => {
377 let idx = self.idx;
378 Transition::Multi(vec![
379 Transition::Pop,
380 Transition::ModifyState(Box::new(move |state, ctx, _| {
381 let editor = state.downcast_mut::<StoryMapEditor>().unwrap();
382 editor.story.markers.remove(idx);
383
384 editor.dirty = true;
385 editor.rebuild_panel(ctx);
386 editor.rebuild_world(ctx);
387 })),
388 ])
389 }
390 _ => unreachable!(),
391 }
392 }
393
394 fn draw_baselayer(&self) -> DrawBaselayer {
395 DrawBaselayer::PreviousState
396 }
397}
398
399struct DrawFreehand {
400 lasso: Lasso,
401 new_idx: usize,
402}
403
404impl State<App> for DrawFreehand {
405 fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
406 if let Some(polygon) = self.lasso.event(ctx) {
407 let idx = self.new_idx;
408 return Transition::Multi(vec![
409 Transition::Pop,
410 Transition::ModifyState(Box::new(move |state, ctx, _| {
411 let editor = state.downcast_mut::<StoryMapEditor>().unwrap();
412 editor.story.markers.push(Marker {
413 pts: polygon.into_outer_ring().into_points(),
414 label: String::new(),
415 });
416
417 editor.dirty = true;
418 editor.rebuild_panel(ctx);
419 editor.rebuild_world(ctx);
420 })),
421 Transition::Push(EditingMarker::new_state(ctx, idx, "new marker")),
422 ]);
423 }
424
425 Transition::Keep
426 }
427
428 fn draw_baselayer(&self) -> DrawBaselayer {
429 DrawBaselayer::PreviousState
430 }
431
432 fn draw(&self, g: &mut GfxCtx, _: &App) {
433 self.lasso.draw(g);
434 }
435}