geolite 0.0.17

open source geocode solution
use geo::{BoundingRect, Geometry, Line};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::symbols::Marker;
use ratatui::widgets::Widget;
use ratatui::widgets::canvas::{self, Canvas, Context};

use crate::admin_level::geometry::outline_of;

const TEXT_COLUMNS: u16 = 98;
const TEXT_ROWS: u16 = 24;
const MARGIN: f64 = 0.05;
const CELL_HEIGHT_IN_WIDTHS: f64 = 2.0;
const SCALE_PER_STEP: f64 = 2.0;
const CLOSEST_STEP: i8 = 16;
const METERS_PER_DEGREE: f64 = 111_320.0;

#[derive(Clone, Copy, Default)]
pub struct zoom {
  steps: i8,
}

impl zoom {
  pub fn closer(self) -> zoom {
    zoom {
      steps: (self.steps + 1).min(CLOSEST_STEP),
    }
  }

  pub fn farther(self) -> zoom {
    zoom {
      steps: self.steps.saturating_sub(1),
    }
  }

  fn scale(self) -> f64 {
    SCALE_PER_STEP.powi(i32::from(self.steps))
  }
}

#[derive(Clone, Copy)]
struct extent {
  min_x: f64,
  min_y: f64,
  max_x: f64,
  max_y: f64,
}

impl extent {
  fn of(lines: &[Line<f64>]) -> Option<extent> {
    let mut coords = lines.iter().flat_map(|line| [line.start, line.end]);
    let first = coords.next()?;
    let start = extent {
      min_x: first.x,
      min_y: first.y,
      max_x: first.x,
      max_y: first.y,
    };
    Some(coords.fold(start, |extent, coord| extent {
      min_x: extent.min_x.min(coord.x),
      min_y: extent.min_y.min(coord.y),
      max_x: extent.max_x.max(coord.x),
      max_y: extent.max_y.max(coord.y),
    }))
  }

  fn with(self, other: extent) -> extent {
    extent {
      min_x: self.min_x.min(other.min_x),
      min_y: self.min_y.min(other.min_y),
      max_x: self.max_x.max(other.max_x),
      max_y: self.max_y.max(other.max_y),
    }
  }

  fn centre(self) -> (f64, f64) {
    (
      (self.min_x + self.max_x) / 2.0,
      (self.min_y + self.max_y) / 2.0,
    )
  }

  fn per_cell(self, cells: &cells) -> f64 {
    let across = (self.max_x - self.min_x) / cells.columns;
    let down = (self.max_y - self.min_y) / cells.rows;
    across.max(down).max(f64::EPSILON) * (1.0 + 2.0 * MARGIN)
  }
}

struct shape {
  lines: Vec<Line<f64>>,
  extent: extent,
}

impl shape {
  fn of(geometry: &Geometry<f64>, stretch: f64) -> Option<shape> {
    let lines: Vec<Line<f64>> = outline_of(geometry)
      .into_iter()
      .flat_map(|line| line.lines())
      .map(|line| {
        Line::new(
          (line.start.x * stretch, line.start.y),
          (line.end.x * stretch, line.end.y),
        )
      })
      .collect();
    let extent = extent::of(&lines)?;
    Some(shape { lines, extent })
  }
}

pub struct drawing {
  leaf: shape,
  above: Vec<shape>,
}

impl drawing {
  pub fn of(leaf: &Geometry<f64>, above: &[&Geometry<f64>]) -> Option<drawing> {
    let nearest = above.first().copied().unwrap_or(leaf);
    let stretch = nearest.bounding_rect()?.center().y.to_radians().cos();
    Some(drawing {
      leaf: shape::of(leaf, stretch)?,
      above: above
        .iter()
        .filter_map(|area| shape::of(area, stretch))
        .collect(),
    })
  }

  fn nearest(&self) -> extent {
    self.above.first().unwrap_or(&self.leaf).extent
  }

  fn whole(&self) -> extent {
    self
      .above
      .iter()
      .fold(self.leaf.extent, |whole, area| whole.with(area.extent))
  }
}

struct cells {
  columns: f64,
  rows: f64,
}

impl cells {
  fn of(area: Rect) -> cells {
    cells {
      columns: f64::from(area.width.max(1)),
      rows: f64::from(area.height.max(1)) * CELL_HEIGHT_IN_WIDTHS,
    }
  }
}

struct frame {
  x: [f64; 2],
  y: [f64; 2],
}

fn span(held: f64, half: f64, (min, max): (f64, f64)) -> [f64; 2] {
  let pad = (max - min) * MARGIN;
  let (low, high) = (min - pad + half, max + pad - half);
  let centre = if low >= high { (min + max) / 2.0 } else { held.clamp(low, high) };
  [centre - half, centre + half]
}

impl frame {
  fn around(drawing: &drawing, area: Rect, zoom: zoom) -> frame {
    let cells = cells::of(area);
    let nearest = drawing.nearest();
    let whole = drawing.whole();
    let at_start = nearest.per_cell(&cells);
    let per_cell = (at_start / zoom.scale()).min(whole.per_cell(&cells));
    let scale = at_start / per_cell;
    let (held_x, held_y) = drawing.leaf.extent.centre();
    let (start_x, start_y) = nearest.centre();
    frame {
      x: span(
        held_x + (start_x - held_x) / scale,
        per_cell * cells.columns / 2.0,
        (whole.min_x, whole.max_x),
      ),
      y: span(
        held_y + (start_y - held_y) / scale,
        per_cell * cells.rows / 2.0,
        (whole.min_y, whole.max_y),
      ),
    }
  }

  fn ground_width(&self) -> String {
    let meters = (self.x[1] - self.x[0]) * METERS_PER_DEGREE;
    if meters < 1_000.0 {
      format!("{meters:.0} m")
    } else if meters < 100_000.0 {
      format!("{:.1} km", meters / 1_000.0)
    } else {
      format!("{:.0} km", meters / 1_000.0)
    }
  }
}

pub fn settled(drawing: &drawing, area: Rect, zoom: zoom) -> zoom {
  let cells = cells::of(area);
  let out = drawing.whole().per_cell(&cells) / drawing.nearest().per_cell(&cells);
  let farthest = -(out.log2().ceil().max(0.0) as i8);
  zoom {
    steps: zoom.steps.max(farthest),
  }
}

pub fn ground_width(drawing: &drawing, area: Rect, zoom: zoom) -> String {
  frame::around(drawing, area, zoom).ground_width()
}

fn draw(ctx: &mut Context, shape: &shape, color: Color) {
  for line in &shape.lines {
    ctx.draw(&canvas::Line::new(
      line.start.x,
      line.start.y,
      line.end.x,
      line.end.y,
      color,
    ));
  }
}

pub fn render(drawing: &drawing, zoom: zoom, area: Rect, buffer: &mut Buffer) {
  let frame = frame::around(drawing, area, zoom);
  Canvas::default()
    .marker(Marker::Braille)
    .x_bounds(frame.x)
    .y_bounds(frame.y)
    .paint(|ctx| {
      for area in &drawing.above {
        draw(ctx, area, Color::DarkGray);
      }
      ctx.marker(Marker::HalfBlock);
      draw(ctx, &drawing.leaf, Color::Reset);
    })
    .render(area, buffer);
}

pub fn text(drawing: &drawing) -> Vec<String> {
  let area = Rect::new(0, 0, TEXT_COLUMNS, TEXT_ROWS);
  let mut buffer = Buffer::empty(area);
  render(drawing, zoom::default(), area, &mut buffer);
  (0..area.height)
    .map(|row| {
      let line: String = (0..area.width)
        .map(|column| buffer[(column, row)].symbol())
        .collect();
      line.trim_end().to_string()
    })
    .collect()
}