use crate::Tile;
use cairo::Context;
use n18hex::consts::PI;
use n18hex::{
Colour, Coord, Hex, HexColour, HexCorner, HexFace, HexPosition,
Orientation,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Label {
City(String),
CityKind(String),
TileName,
MapLocation(String),
Note(String),
Revenue(usize),
PhaseRevenue(Vec<(HexColour, usize, bool)>),
PhaseRevenueVert(Vec<(HexColour, usize, bool)>),
}
impl Label {
pub fn y() -> Self {
Label::CityKind("Y".to_string())
}
pub fn is_tile_restriction(&self) -> bool {
match self {
Self::City(_) => true,
Self::CityKind(_) => true,
Self::TileName => false,
Self::MapLocation(_) => false,
Self::Revenue(_) => false,
Self::PhaseRevenue(_) => false,
Self::PhaseRevenueVert(_) => false,
Self::Note(_) => false,
}
}
pub fn draw(
&self,
ctx: &Context,
hex: &Hex,
pos: &HexPosition,
tile: &Tile,
) {
let style = match self {
Self::City(_) => &hex.theme.city_label,
Self::CityKind(_) => &hex.theme.city_kind_label,
Self::TileName => &hex.theme.tile_label,
Self::MapLocation(_) => &hex.theme.location_label,
Self::Revenue(_) => &hex.theme.revenue_label,
Self::PhaseRevenue(_) => &hex.theme.phase_revenue_label,
Self::PhaseRevenueVert(_) => &hex.theme.phase_revenue_label,
Self::Note(_) => &hex.theme.note_label,
};
let mut labeller = style.labeller(ctx, hex);
let coord = pos.coord(hex);
let horiz = h_align(hex, pos);
let vert = v_align(hex, pos);
labeller.halign(horiz);
labeller.valign(vert);
match self {
Self::City(text) | Self::CityKind(text) | Self::Note(text) => {
labeller.draw(text, coord);
}
Self::TileName => {
let label_text = &tile.name;
labeller.draw(label_text, coord);
}
Self::MapLocation(name) => {
let colour = if tile.colour == HexColour::Red
|| tile.colour == HexColour::Blue
{
Colour::WHITE
} else {
Colour::BLACK
};
labeller.colour(colour);
labeller.draw(name, coord);
}
Self::Revenue(amount_ix) => {
let amount = tile.revenues()[*amount_ix];
let label_text = format!("{}", amount);
let text_size = labeller.size(&label_text);
let ratio = text_size.width / text_size.height;
let radius = ELLIPSE_RADIUS_SCALE
* (0.5 * text_size.width).max(0.5 * text_size.height);
let width = 2.0 * radius;
let height = ellipse_height(radius, ratio);
let ellipse_size = n18hex::theme::Size {
dx: 0.0,
dy: 0.0,
width,
height,
};
let origin = ellipse_size.top_left(&coord, horiz, vert);
let centre = Coord::from((
origin.x + 0.5 * width,
origin.y + 0.5 * height,
));
ctx.new_path();
define_ellipse(ctx, radius, ratio, centre);
hex.theme.label_circle.apply_fill(ctx);
ctx.fill_preserve().unwrap();
hex.theme.label_circle.apply_line_and_stroke(ctx, hex);
ctx.stroke().unwrap();
ctx.new_path();
labeller.halign(n18hex::theme::AlignH::Centre);
labeller.valign(n18hex::theme::AlignV::Middle);
labeller.draw(&label_text, centre);
}
Self::PhaseRevenue(amounts) => {
let boxes = get_boxes(amounts);
let active_box =
active_box_ix(amounts).expect("No active revenue box");
let (box_width, box_height) =
box_dims(&boxes, hex, &labeller);
let net_width = box_width * boxes.len() as f64;
let mut origin = coord;
origin.x += horiz.hjust() * net_width;
origin.y += vert.vjust() * box_height;
labeller.halign(n18hex::theme::AlignH::Left);
labeller.valign(n18hex::theme::AlignV::Top);
boxes.iter().enumerate().for_each(
|(ix, (bg_colour, text))| {
let dx = ix as f64 * box_width;
let x0 = origin.x + dx;
let y0 = origin.y;
ctx.rectangle(x0, y0, box_width, box_height);
hex.theme.apply_hex_colour(ctx, *bg_colour);
ctx.fill().unwrap();
let size = labeller.size(text);
let x = x0 - size.dx + 0.5 * (box_width - size.width);
let y =
y0 - size.dy + 0.5 * (box_height - size.height);
labeller.draw(text, (x, y).into());
},
);
let dx = active_box as f64 * box_width;
ctx.rectangle(origin.x + dx, origin.y, box_width, box_height);
Colour::BLACK.apply_colour(ctx);
ctx.stroke().unwrap();
}
Self::PhaseRevenueVert(amounts) => {
let boxes = get_boxes(amounts);
let active_box =
active_box_ix(amounts).expect("No active revenue box");
let (box_width, box_height) =
box_dims(&boxes, hex, &labeller);
let net_height = box_height * boxes.len() as f64;
let mut origin = coord;
origin.x += horiz.hjust() * box_width;
origin.y += vert.vjust() * net_height;
labeller.halign(n18hex::theme::AlignH::Left);
labeller.valign(n18hex::theme::AlignV::Top);
boxes.iter().enumerate().for_each(
|(ix, (bg_colour, text))| {
let dy = ix as f64 * box_height;
let x0 = origin.x;
let y0 = origin.y + dy;
ctx.rectangle(x0, y0, box_width, box_height);
hex.theme.apply_hex_colour(ctx, *bg_colour);
ctx.fill().unwrap();
let size = labeller.size(text);
let x = x0 - size.dx + 0.5 * (box_width - size.width);
let y =
y0 - size.dy + 0.5 * (box_height - size.height);
labeller.draw(text, (x, y).into());
},
);
let dx = active_box as f64 * box_height;
ctx.rectangle(origin.x, origin.y + dx, box_width, box_height);
Colour::BLACK.apply_colour(ctx);
ctx.stroke().unwrap();
}
};
}
pub fn draw_custom_tile_name(ctx: &Context, hex: &Hex, name: &str) {
let mut labeller = hex.theme.tile_label.labeller(ctx, hex);
let pos = Self::tile_name_position(hex);
let coord = pos.coord(hex);
let horiz = h_align(hex, &pos);
let vert = v_align(hex, &pos);
labeller.halign(horiz);
labeller.valign(vert);
labeller.draw(name, coord);
}
pub fn tile_name_position(hex: &Hex) -> HexPosition {
match hex.orientation() {
Orientation::FlatTop => {
HexPosition::Corner(HexCorner::BottomRight, None)
}
Orientation::PointedTop => {
HexPosition::from(HexCorner::BottomRight).to_centre(0.05)
}
}
}
}
fn get_boxes(
amounts: &[(HexColour, usize, bool)],
) -> Vec<(HexColour, String)> {
amounts
.iter()
.map(|(colour, amount, _active)| (*colour, format!("{}", amount)))
.collect()
}
fn active_box_ix(amounts: &[(HexColour, usize, bool)]) -> Option<usize> {
amounts.iter().enumerate().find_map(
|(ix, (_colour, _amount, active))| {
if *active {
Some(ix)
} else {
None
}
},
)
}
fn box_dims(
boxes: &[(HexColour, String)],
hex: &Hex,
labeller: &n18hex::theme::Labeller<'_>,
) -> (f64, f64) {
let (box_width, box_height) = boxes
.iter()
.map(|(_colour, text)| {
let size = labeller.size(text);
(size.width, size.height)
})
.fold(
(0.0, 0.0),
|(curr_w, curr_h): (f64, f64), (new_w, new_h)| {
(curr_w.max(new_w), curr_h.max(new_h))
},
);
let margin_width = hex.theme.phase_revenue_margin_x.absolute(hex);
let margin_height = hex.theme.phase_revenue_margin_y.absolute(hex);
let box_width = box_width + 2.0 * margin_width;
let box_height = box_height + 2.0 * margin_height;
(box_width, box_height)
}
fn h_align(hex: &Hex, pos: &HexPosition) -> n18hex::theme::AlignH {
use n18hex::theme::AlignH;
use HexCorner::*;
use HexFace::*;
use HexPosition::*;
match hex.orientation() {
Orientation::FlatTop => match pos {
Centre(_) => AlignH::Centre,
Face(Bottom, _) | Face(Top, _) => AlignH::Centre,
Face(LowerLeft, _)
| Face(UpperLeft, _)
| Corner(BottomLeft, _)
| Corner(Left, _)
| Corner(TopLeft, _) => AlignH::Left,
Face(LowerRight, _)
| Face(UpperRight, _)
| Corner(BottomRight, _)
| Corner(Right, _)
| Corner(TopRight, _) => AlignH::Right,
},
Orientation::PointedTop => match pos {
Centre(_) => AlignH::Centre,
Corner(TopLeft, _) | Corner(BottomRight, _) => AlignH::Centre,
Face(Bottom, _)
| Face(LowerLeft, _)
| Face(UpperLeft, _)
| Corner(BottomLeft, _)
| Corner(Left, _) => AlignH::Left,
Face(Top, _)
| Face(LowerRight, _)
| Face(UpperRight, _)
| Corner(Right, _)
| Corner(TopRight, _) => AlignH::Right,
},
}
}
fn v_align(hex: &Hex, pos: &HexPosition) -> n18hex::theme::AlignV {
use n18hex::theme::AlignV;
use HexCorner::*;
use HexFace::*;
use HexPosition::*;
match hex.orientation() {
Orientation::FlatTop => match pos {
Centre(_) => AlignV::Middle,
Corner(Left, _) | Corner(Right, _) => AlignV::Middle,
Face(UpperLeft, _)
| Face(Top, _)
| Face(UpperRight, _)
| Corner(TopLeft, _)
| Corner(TopRight, _) => AlignV::Top,
Face(LowerLeft, _)
| Face(Bottom, _)
| Face(LowerRight, _)
| Corner(BottomLeft, _)
| Corner(BottomRight, _) => AlignV::Bottom,
},
Orientation::PointedTop => match pos {
Centre(_) => AlignV::Middle,
Face(LowerLeft, _) | Face(UpperRight, _) => AlignV::Middle,
Face(UpperLeft, _)
| Face(Top, _)
| Corner(Left, _)
| Corner(TopLeft, _)
| Corner(TopRight, _) => AlignV::Top,
Face(Bottom, _)
| Face(LowerRight, _)
| Corner(BottomLeft, _)
| Corner(BottomRight, _)
| Corner(Right, _) => AlignV::Bottom,
},
}
}
const ELLIPSE_RATIO: f64 = 1.25;
const ELLIPSE_RADIUS_SCALE: f64 = 4.0 / 3.0;
fn ellipse_height(radius: f64, ratio: f64) -> f64 {
let diam = 2.0 * radius;
if ratio >= ELLIPSE_RATIO {
diam / ratio
} else {
diam
}
}
fn define_ellipse(ctx: &Context, radius: f64, ratio: f64, centre: Coord) {
if ratio >= ELLIPSE_RATIO {
let matrix = ctx.matrix();
let scale = 1.0 / ratio;
ctx.scale(1.0, scale);
ctx.arc(centre.x, centre.y / scale, radius, 0.0, 2.0 * PI);
ctx.set_matrix(matrix);
} else {
ctx.arc(centre.x, centre.y, radius, 0.0, 2.0 * PI);
};
}