1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use std::cell::RefCell;

use geom::{Circle, Distance, PolyLine, Pt2D};

use crate::{Color, Drawable, GeomBatch, GfxCtx};

/// Draw `Circles` and `PolyLines` in map-space that scale their size as the canvas is zoomed. The
/// goal is to appear with roughly constant screen-space size, but for the moment, this is
/// approximated by discretizing into 10 buckets. The scaling only happens when the canvas is
/// zoomed out less than a value of 1.0.
pub struct DrawUnzoomedShapes {
    shapes: Vec<Shape>,
    per_zoom: RefCell<[Option<Drawable>; 11]>,
}

enum Shape {
    Line {
        polyline: PolyLine,
        width: Distance,
        color: Color,
    },
    Circle {
        center: Pt2D,
        radius: Distance,
        color: Color,
    },
    Custom(Box<dyn Fn(&mut GeomBatch, f64)>),
}

impl Shape {
    fn render(&self, batch: &mut GeomBatch, thickness: f64) {
        match self {
            Shape::Line {
                polyline,
                width,
                color,
            } => {
                batch.push(*color, polyline.make_polygons(thickness * *width));
            }
            Shape::Circle {
                center,
                radius,
                color,
            } => {
                // TODO Here especially if we're drawing lots of circles with the same radius,
                // generating the shape once and translating it is much more efficient.
                // UnzoomedAgents does this.
                batch.push(
                    *color,
                    Circle::new(*center, thickness * *radius).to_polygon(),
                );
            }
            Shape::Custom(f) => f(batch, thickness),
        }
    }
}

pub struct DrawUnzoomedShapesBuilder {
    shapes: Vec<Shape>,
}

impl DrawUnzoomedShapes {
    pub fn empty() -> Self {
        Self {
            shapes: Vec::new(),
            per_zoom: Default::default(),
        }
    }

    pub fn builder() -> DrawUnzoomedShapesBuilder {
        DrawUnzoomedShapesBuilder { shapes: Vec::new() }
    }

    pub fn draw(&self, g: &mut GfxCtx) {
        let (zoom, idx) = discretize_zoom(g.canvas.cam_zoom);
        let value = &mut self.per_zoom.borrow_mut()[idx];
        if value.is_none() {
            // Never shrink past the original size -- always at least 1.0.
            // zoom ranges between [0.0, 1.0], and we want thicker shapes as zoom approaches 0.
            let max = 5.0;
            // So thickness ranges between [1.0, 5.0]
            let thickness = 1.0 + (max - 1.0) * (1.0 - zoom);

            let mut batch = GeomBatch::new();
            for shape in &self.shapes {
                shape.render(&mut batch, thickness);
            }
            *value = Some(g.upload(batch));
        }
        g.redraw(value.as_ref().unwrap());
    }
}

impl DrawUnzoomedShapesBuilder {
    pub fn add_line(&mut self, polyline: PolyLine, width: Distance, color: Color) {
        self.shapes.push(Shape::Line {
            polyline,
            width,
            color,
        });
    }

    pub fn add_circle(&mut self, center: Pt2D, radius: Distance, color: Color) {
        self.shapes.push(Shape::Circle {
            center,
            radius,
            color,
        });
    }

    /// Custom drawing code can add anything it wants to a batch, using a specified thickness in
    /// the [1.0, 5.0] range
    pub fn add_custom(&mut self, f: Box<dyn Fn(&mut GeomBatch, f64)>) {
        self.shapes.push(Shape::Custom(f));
    }

    // TODO We might take EventCtx here to upload something to the GPU.
    pub fn build(self) -> DrawUnzoomedShapes {
        DrawUnzoomedShapes {
            shapes: self.shapes,
            per_zoom: Default::default(),
        }
    }
}

// Continuously changing road width as we zoom looks great, but it's terribly slow. We'd have to
// move line thickening into the shader to do it better. So recalculate with less granularity.
//
// Returns ([0.0, 1.0], [0, 10])
fn discretize_zoom(zoom: f64) -> (f64, usize) {
    if zoom >= 1.0 {
        return (1.0, 10);
    }
    let rounded = (zoom * 10.0).round();
    let idx = rounded as usize;
    (rounded / 10.0, idx)
}