1use geom::{Angle, Bounds, GPSBounds, Polygon, Pt2D, Tessellation};
2
3use crate::{
4 svg, Color, DeferDraw, Drawable, EventCtx, Fill, GfxCtx, JustDraw, Prerender, ScreenDims,
5 Widget,
6};
7
8pub mod geom_batch_stack;
9
10#[derive(Clone)]
12pub struct GeomBatch {
13 pub(crate) list: Vec<(Fill, Tessellation, f64)>,
16 pub autocrop_dims: bool,
17}
18
19impl std::fmt::Debug for GeomBatch {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 f.debug_struct("GeomBatch")
22 .field("bounds", &self.get_bounds())
23 .field("items", &self.list.len())
24 .field("autocrop_dims", &self.autocrop_dims)
25 .finish()
26 }
27}
28
29impl GeomBatch {
30 pub fn new() -> GeomBatch {
32 GeomBatch {
33 list: Vec::new(),
34 autocrop_dims: true,
35 }
36 }
37
38 pub fn push<F: Into<Fill>, T: Into<Tessellation>>(&mut self, fill: F, p: T) {
40 self.push_with_z(fill, p, 0.0);
41 }
42
43 pub fn push_with_z<F: Into<Fill>, T: Into<Tessellation>>(
47 &mut self,
48 fill: F,
49 p: T,
50 z_offset: f64,
51 ) {
52 debug_assert!(z_offset > -1.0);
53 debug_assert!(z_offset <= 0.0);
54 self.list.push((fill.into(), p.into(), z_offset));
55 }
56
57 pub fn unshift<F: Into<Fill>, T: Into<Tessellation>>(&mut self, fill: F, p: T) {
59 self.list.insert(0, (fill.into(), p.into(), 0.0));
60 }
61
62 pub fn shift(&mut self) {
64 self.list.remove(0);
65 }
66
67 pub fn extend<F: Into<Fill>, T: Into<Tessellation>>(&mut self, fill: F, polys: Vec<T>) {
69 let fill = fill.into();
70 for p in polys {
71 self.list.push((fill.clone(), p.into(), 0.0));
72 }
73 }
74
75 pub fn append(&mut self, other: GeomBatch) {
77 self.list.extend(other.list);
78 }
79
80 pub fn consume(self) -> Vec<(Fill, Tessellation, f64)> {
82 self.list
83 }
84
85 pub fn draw(self, g: &mut GfxCtx) {
87 let obj = g.prerender.upload_temporary(self);
88 g.redraw(&obj);
89 }
90
91 pub fn upload(self, ctx: &EventCtx) -> Drawable {
94 ctx.prerender.upload(self)
95 }
96
97 pub fn batch(self) -> Widget {
99 DeferDraw::new_widget(self)
100 }
101
102 pub fn into_widget(self, ctx: &EventCtx) -> Widget {
104 JustDraw::wrap(ctx, self)
105 }
106
107 pub fn get_bounds(&self) -> Bounds {
109 let mut bounds = Bounds::new();
110 for (_, poly, _) in &self.list {
111 bounds.union(poly.get_bounds());
112 }
113 if !self.autocrop_dims {
114 bounds.update(Pt2D::new(0.0, 0.0));
115 }
116 bounds
117 }
118
119 pub fn autocrop(mut self) -> GeomBatch {
121 let bounds = self.get_bounds();
122 if bounds.min_x == 0.0 && bounds.min_y == 0.0 {
123 return self;
124 }
125 for (_, poly, _) in &mut self.list {
126 poly.translate(-bounds.min_x, -bounds.min_y);
127 }
128 self
129 }
130
131 pub fn is_empty(&self) -> bool {
133 self.list.is_empty()
134 }
135
136 pub fn get_dims(&self) -> ScreenDims {
138 if self.is_empty() {
141 return ScreenDims::new(0.0, 0.0);
142 }
143 let bounds = self.get_bounds();
144 ScreenDims::new(bounds.width(), bounds.height())
145 }
146
147 pub fn load_svg<P: AsRef<Prerender>, I: AsRef<str>>(prerender: &P, filename: I) -> GeomBatch {
149 svg::load_svg(prerender.as_ref(), filename.as_ref()).0
150 }
151
152 pub fn load_svg_bytes<P: AsRef<Prerender>>(
154 prerender: &P,
155 labeled_bytes: (&str, &[u8]),
156 ) -> GeomBatch {
157 svg::load_svg_bytes(prerender.as_ref(), labeled_bytes.0, labeled_bytes.1)
158 .expect("invalid svg bytes")
159 .0
160 }
161
162 pub fn load_svg_bytes_uncached(raw: &[u8]) -> GeomBatch {
168 svg::load_svg_from_bytes_uncached(raw).unwrap().0
169 }
170
171 pub fn color(mut self, transformation: RewriteColor) -> GeomBatch {
173 for (fancy, _, _) in &mut self.list {
174 if let Fill::Color(ref mut c) = fancy {
175 *c = transformation.apply(*c);
176 }
177 }
178 self
179 }
180
181 pub fn centered_on(self, center: Pt2D) -> GeomBatch {
183 let dims = self.get_dims();
184 let dx = center.x() - dims.width / 2.0;
185 let dy = center.y() - dims.height / 2.0;
186 self.translate(dx, dy)
187 }
188
189 pub fn translate(mut self, dx: f64, dy: f64) -> GeomBatch {
191 for (_, poly, _) in &mut self.list {
192 poly.translate(dx, dy);
193 }
194 self
195 }
196
197 pub fn rotate(mut self, angle: Angle) -> GeomBatch {
199 for (_, poly, _) in &mut self.list {
200 poly.rotate(angle);
201 }
202 self
203 }
204
205 pub fn rotate_around_batch_center(mut self, angle: Angle) -> GeomBatch {
207 if self.list.is_empty() {
209 return self;
210 }
211
212 let center = self.get_bounds().center();
213 for (_, poly, _) in &mut self.list {
214 poly.rotate_around(angle, center);
215 }
216 self
217 }
218
219 pub fn multi_transform(mut self, scale: f64, center_on: Pt2D, rotate: Angle) -> GeomBatch {
222 if self.list.is_empty() {
223 return self;
224 }
225
226 let bounds = self.get_bounds().scale(scale);
227 let dx = center_on.x() - bounds.width() / 2.0;
228 let dy = center_on.y() - bounds.height() / 2.0;
229 let rotate_around_pt = bounds.center().offset(dx, dy);
230
231 for (_, poly, _) in &mut self.list {
232 poly.inplace_multi_transform(scale, dx, dy, rotate, rotate_around_pt);
233 }
234
235 self
236 }
237
238 pub fn scale(self, factor: f64) -> GeomBatch {
240 self.scale_xy(factor, factor)
241 }
242
243 pub fn scale_xy(mut self, x_factor: f64, y_factor: f64) -> GeomBatch {
244 #[allow(clippy::float_cmp)]
245 if x_factor == 1.0 && y_factor == 1.0 {
246 return self;
247 }
248
249 for (_, poly, _) in &mut self.list {
250 poly.scale_xy(x_factor, y_factor);
251 }
252 self
253 }
254
255 pub fn scale_to_fit_width(self, width: f64) -> GeomBatch {
257 let ratio = width / self.get_bounds().width();
258 self.scale(ratio)
259 }
260
261 pub fn scale_to_fit_height(self, height: f64) -> GeomBatch {
263 let ratio = height / self.get_bounds().height();
264 self.scale(ratio)
265 }
266
267 pub fn scale_to_fit_square(self, dims: f64) -> GeomBatch {
269 let ratio1 = dims / self.get_bounds().width();
270 let ratio2 = dims / self.get_bounds().height();
271 self.scale(ratio1.min(ratio2))
272 }
273
274 pub fn set_z_offset(mut self, offset: f64) -> GeomBatch {
277 if offset <= -1.0 || offset > 0.0 {
278 panic!("set_z_offset({}) must be in (-1, 0]", offset);
279 }
280 for (_, _, z) in &mut self.list {
281 *z = offset;
282 }
283 self
284 }
285
286 pub fn into_geojson(self, gps_bounds: Option<&GPSBounds>) -> Vec<geojson::Feature> {
291 let mut features = Vec::new();
292 for (fill, polygon, _) in self.list {
293 if let Fill::Color(color) = fill {
294 let mut properties = serde_json::Map::new();
295 properties.insert("color".to_string(), color.as_hex().into());
296 for triangle in polygon.triangles() {
297 features.push(geojson::Feature {
298 bbox: None,
299 geometry: Some(Polygon::from_triangle(&triangle).to_geojson(gps_bounds)),
301 id: None,
302 properties: Some(properties.clone()),
303 foreign_members: None,
304 });
305 }
306 }
307 }
308 features
309 }
310
311 pub fn build(self, ctx: &EventCtx) -> Drawable {
312 ctx.upload(self)
313 }
314}
315
316impl Default for GeomBatch {
317 fn default() -> Self {
318 GeomBatch::new()
319 }
320}
321
322impl<F: Into<Fill>, T: Into<Tessellation>> From<Vec<(F, T)>> for GeomBatch {
323 fn from(list: Vec<(F, T)>) -> GeomBatch {
325 GeomBatch {
326 list: list
327 .into_iter()
328 .map(|(c, p)| (c.into(), p.into(), 0.0))
329 .collect(),
330 autocrop_dims: true,
331 }
332 }
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
337pub enum RewriteColor {
338 NoOp,
340 Change(Color, Color),
342 ChangeAll(Color),
345 ChangeAlpha(f32),
347 MakeGrayscale,
349}
350
351impl std::convert::From<Color> for RewriteColor {
352 fn from(color: Color) -> RewriteColor {
353 RewriteColor::ChangeAll(color)
354 }
355}
356
357impl RewriteColor {
358 fn apply(&self, c: Color) -> Color {
359 match self {
360 RewriteColor::NoOp => c,
361 RewriteColor::Change(from, to) => {
362 if c == *from {
363 *to
364 } else {
365 c
366 }
367 }
368 RewriteColor::ChangeAll(to) => {
369 if c == Color::CLEAR {
370 c
371 } else {
372 *to
373 }
374 }
375 RewriteColor::ChangeAlpha(alpha) => c.alpha(*alpha),
376 RewriteColor::MakeGrayscale => {
377 let avg = (c.r + c.g + c.b) / 3.0;
378 Color::grey(avg).alpha(c.a)
379 }
380 }
381 }
382}