use crate::layers::{Layer, serde_color32};
use crate::projection::{GeoPos, MapProjection};
use egui::{Align2, Color32, FontId, Painter, Pos2, Rect, Response};
use serde::{Deserialize, Serialize};
use std::any::Any;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum TextSize {
Static(f32),
Relative(f32),
}
impl Default for TextSize {
fn default() -> Self {
Self::Static(12.0)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Text {
pub text: String,
pub pos: GeoPos,
pub size: TextSize,
#[serde(with = "serde_color32")]
pub color: Color32,
#[serde(with = "serde_color32")]
pub background: Color32,
}
impl Default for Text {
fn default() -> Self {
Self {
text: "New Text".to_string(),
pos: GeoPos { lon: 0.0, lat: 0.0 }, size: TextSize::default(),
color: Color32::BLACK,
background: Color32::from_rgba_unmultiplied(255, 255, 255, 180),
}
}
}
#[derive(Clone, Debug)]
pub struct EditingText {
pub index: Option<usize>,
pub properties: Text,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextLayerMode {
#[default]
Disabled,
Modify,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(default)]
#[derive(Default)]
pub struct TextLayer {
texts: Vec<Text>,
#[serde(skip)]
pub mode: TextLayerMode,
#[serde(skip)]
pub new_text_properties: Text,
#[serde(skip)]
pub editing: Option<EditingText>,
#[serde(skip)]
dragged_text_index: Option<usize>,
}
impl TextLayer {
pub fn start_editing(&mut self, index: usize) {
if let Some(text) = self.texts.get(index) {
self.editing = Some(EditingText {
index: Some(index),
properties: text.clone(),
});
}
}
pub fn delete(&mut self, index: usize) {
if index < self.texts.len() {
self.texts.remove(index);
}
}
pub fn commit_edit(&mut self) {
if let Some(editing) = self.editing.take() {
if let Some(index) = editing.index {
if let Some(text) = self.texts.get_mut(index) {
*text = editing.properties;
}
} else {
self.texts.push(editing.properties);
}
}
}
pub fn cancel_edit(&mut self) {
self.editing = None;
}
#[cfg(feature = "geojson")]
pub fn to_geojson_str(&self) -> Result<String, serde_json::Error> {
let features: Vec<geojson::Feature> = self
.texts
.clone()
.into_iter()
.map(geojson::Feature::from)
.collect();
let feature_collection = geojson::FeatureCollection {
bbox: None,
features,
foreign_members: None,
};
serde_json::to_string(&feature_collection)
}
#[cfg(feature = "geojson")]
pub fn from_geojson_str(&mut self, s: &str) -> Result<(), serde_json::Error> {
let feature_collection: geojson::FeatureCollection = serde_json::from_str(s)?;
let new_texts: Vec<Text> = feature_collection
.features
.into_iter()
.filter_map(|f| Text::try_from(f).ok())
.collect();
self.texts.extend(new_texts);
Ok(())
}
fn handle_modify_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
if self.editing.is_some() {
return response.hovered();
}
if response.drag_started()
&& let Some(pointer_pos) = response.interact_pointer_pos()
{
self.dragged_text_index = self.find_text_at(pointer_pos, projection, &response.ctx);
}
if response.dragged()
&& let Some(text_index) = self.dragged_text_index
&& let Some(text) = self.texts.get_mut(text_index)
&& let Some(pointer_pos) = response.interact_pointer_pos()
{
text.pos = projection.unproject(pointer_pos);
}
if response.drag_stopped() {
self.dragged_text_index = None;
}
if self.dragged_text_index.is_some() {
response.ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
} else if let Some(hover_pos) = response.hover_pos() {
if self
.find_text_at(hover_pos, projection, &response.ctx)
.is_some()
{
response.ctx.set_cursor_icon(egui::CursorIcon::PointingHand);
} else {
response.ctx.set_cursor_icon(egui::CursorIcon::Crosshair);
}
}
if !response.dragged() && response.clicked() {
if let Some(pointer_pos) = response.interact_pointer_pos() {
if let Some(index) = self.find_text_at(pointer_pos, projection, &response.ctx) {
self.start_editing(index);
} else {
let geo_pos = projection.unproject(pointer_pos);
let mut properties = self.new_text_properties.clone();
properties.pos = geo_pos;
self.editing = Some(EditingText {
index: None,
properties,
});
}
}
}
response.hovered()
}
fn find_text_at(
&self,
screen_pos: Pos2,
projection: &MapProjection,
ctx: &egui::Context,
) -> Option<usize> {
self.texts.iter().enumerate().rev().find_map(|(i, text)| {
let text_rect = self.get_text_rect(text, projection, ctx);
if text_rect.expand(5.0).contains(screen_pos) {
Some(i)
} else {
None
}
})
}
}
impl Layer for TextLayer {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn handle_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
match self.mode {
TextLayerMode::Disabled => false,
TextLayerMode::Modify => self.handle_modify_input(response, projection),
}
}
fn draw(&self, painter: &Painter, projection: &MapProjection) {
for text in &self.texts {
let screen_pos = projection.project(text.pos);
let galley = painter.layout_no_wrap(
text.text.clone(),
FontId::proportional(self.get_font_size(text, projection)),
text.color,
);
let rect =
Align2::CENTER_CENTER.anchor_rect(Rect::from_min_size(screen_pos, galley.size()));
painter.rect_filled(rect.expand(2.0), 3.0, text.background);
painter.galley(rect.min, galley, Color32::TRANSPARENT);
}
}
}
impl TextLayer {
fn get_font_size(&self, text: &Text, projection: &MapProjection) -> f32 {
match text.size {
TextSize::Static(size) => size,
TextSize::Relative(size_in_meters) => {
let p2 = projection.project(GeoPos {
lon: text.pos.lon
+ (f64::from(size_in_meters)
/ (111_320.0 * text.pos.lat.to_radians().cos())),
lat: text.pos.lat,
});
(p2.x - projection.project(text.pos).x).abs()
}
}
}
fn get_text_rect(&self, text: &Text, projection: &MapProjection, ctx: &egui::Context) -> Rect {
let font_size = self.get_font_size(text, projection);
let galley = ctx
.debug_painter()
.layout_job(egui::text::LayoutJob::simple(
text.text.clone(),
FontId::proportional(font_size),
text.color,
f32::INFINITY,
));
let screen_pos = projection.project(text.pos);
Align2::CENTER_CENTER.anchor_rect(Rect::from_min_size(screen_pos, galley.size()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_layer_serde() {
let mut layer = TextLayer::default();
layer.mode = TextLayerMode::Modify; layer.texts.push(Text {
text: "Hello".to_string(),
pos: GeoPos { lon: 1.0, lat: 2.0 },
size: TextSize::Static(14.0),
color: Color32::from_rgb(0, 0, 255),
background: Color32::from_rgba_unmultiplied(255, 0, 0, 128),
});
let json = serde_json::to_string(&layer).unwrap();
assert!(json.contains(r##""texts":[{"text":"Hello","pos":{"lon":1.0,"lat":2.0},"size":{"Static":14.0},"color":"#0000ffff","background":"#ff000080""##));
assert!(!json.contains("mode"));
assert!(!json.contains("new_text_properties"));
assert!(!json.contains("editing"));
assert!(!json.contains("dragged_text_index"));
let deserialized: TextLayer = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.texts.len(), 1);
assert_eq!(deserialized.texts[0].text, "Hello");
assert_eq!(deserialized.texts[0].pos, GeoPos { lon: 1.0, lat: 2.0 });
assert_eq!(deserialized.texts[0].size, TextSize::Static(14.0));
assert_eq!(deserialized.texts[0].color, Color32::from_rgb(0, 0, 255));
assert_eq!(
deserialized.texts[0].background,
Color32::from_rgba_unmultiplied(255, 0, 0, 128)
);
let default_layer = TextLayer::default();
assert_eq!(deserialized.mode, default_layer.mode);
assert_eq!(
deserialized.new_text_properties,
default_layer.new_text_properties
);
assert!(deserialized.editing.is_none());
assert!(deserialized.dragged_text_index.is_none());
}
#[cfg(feature = "geojson")]
mod geojson_tests {
use super::*;
#[test]
fn text_layer_geojson() {
let mut layer = TextLayer::default();
layer.texts.push(Text {
text: "Hello".to_string(),
pos: (10.0, 20.0).into(),
size: TextSize::Static(14.0),
color: Color32::from_rgb(0, 0, 255),
background: Color32::from_rgba_unmultiplied(255, 0, 0, 128),
});
let geojson_str = layer.to_geojson_str().unwrap();
let mut new_layer = TextLayer::default();
new_layer.from_geojson_str(&geojson_str).unwrap();
assert_eq!(new_layer.texts.len(), 1);
assert_eq!(layer.texts[0], new_layer.texts[0]);
}
}
}