use crate::layers::Layer;
use crate::projection::{GeoPos, MapProjection};
use egui::{Color32, Painter, PointerButton, Pos2, Response};
use serde::{Deserialize, Serialize};
use std::any::Any;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SvgElement {
pub pos: GeoPos,
pub text: String,
pub metadata: String,
pub scalable: bool,
#[serde(default = "default_true")]
pub clickable: bool,
#[serde(default)]
pub draggable: bool,
#[serde(default = "default_anchor")]
pub anchor: Pos2,
}
fn default_anchor() -> Pos2 {
Pos2::new(0.5, 0.5)
}
fn default_true() -> bool {
true
}
impl SvgElement {
pub fn new(pos: GeoPos, text: impl Into<String>, metadata: impl Into<String>) -> Self {
Self {
pos,
text: text.into(),
metadata: metadata.into(),
scalable: false,
clickable: true,
draggable: false,
anchor: default_anchor(),
}
}
pub fn from_xy(
lon: f64,
lat: f64,
text: impl Into<String>,
metadata: impl Into<String>,
) -> Self {
Self {
pos: GeoPos { lon, lat },
text: text.into(),
metadata: metadata.into(),
scalable: false,
clickable: true,
draggable: false,
anchor: default_anchor(),
}
}
#[must_use]
pub fn with_scalable(mut self, scalable: bool) -> Self {
self.scalable = scalable;
self
}
#[must_use]
pub fn with_clickable(mut self, clickable: bool) -> Self {
self.clickable = clickable;
self
}
#[must_use]
pub fn with_draggable(mut self, draggable: bool) -> Self {
self.draggable = draggable;
self
}
#[must_use]
pub fn with_anchor(mut self, anchor: Pos2) -> Self {
self.anchor = anchor;
self
}
}
#[derive(Clone, Debug)]
pub struct SvgClickEvent {
pub button: PointerButton,
pub metadata: String,
pub world_pos: GeoPos,
pub screen_pos: Pos2,
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct SvgLayer {
pub elements: Vec<SvgElement>,
#[serde(skip)]
pub events: Vec<SvgClickEvent>,
#[serde(skip)]
pub dragging_index: Option<usize>,
}
impl SvgLayer {
pub fn add_element(&mut self, element: SvgElement) {
self.elements.push(element);
}
pub fn clear(&mut self) {
self.elements.clear();
}
pub fn take_events(&mut self) -> Vec<SvgClickEvent> {
std::mem::take(&mut self.events)
}
}
impl Layer for SvgLayer {
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 {
egui_extras::install_image_loaders(&response.ctx);
for element in &self.elements {
let uri = format!("bytes://{}.svg", rust_hash(&element.text));
response
.ctx
.include_bytes(uri, element.text.as_bytes().to_vec());
}
let mut handled = false;
if let Some(index) = self.dragging_index {
if response.dragged() {
if let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some(element) = self.elements.get_mut(index)
{
element.pos = projection.unproject(pointer_pos);
handled = true;
response.ctx.request_repaint();
}
} else {
self.dragging_index = None;
}
}
if let Some(pointer_pos) = response.interact_pointer_pos() {
for (index, element) in self.elements.iter_mut().enumerate() {
if !element.clickable && !element.draggable {
continue;
}
let screen_pos = projection.project(element.pos);
let uri = format!("bytes://{}.svg", rust_hash(&element.text));
if let Ok(egui::load::TexturePoll::Ready { texture }) = response
.ctx
.try_load_texture(&uri, egui::TextureOptions::default(), Default::default())
{
let mut size = texture.size;
if element.scalable {
let scale = 2.0_f32.powi(i32::from(projection.zoom) - 10);
size *= scale;
}
let rect = egui::Rect::from_min_size(
screen_pos - size * element.anchor.to_vec2(),
size,
);
if rect.contains(pointer_pos) {
if element.draggable && response.drag_started() {
self.dragging_index = Some(index);
handled = true;
}
if element.clickable && (response.clicked() || response.secondary_clicked())
{
let button = if response.secondary_clicked() {
PointerButton::Secondary
} else {
PointerButton::Primary
};
self.events.push(SvgClickEvent {
button,
metadata: element.metadata.clone(),
world_pos: projection.unproject(pointer_pos),
screen_pos: pointer_pos,
});
handled = true;
}
}
}
}
}
handled
}
fn draw(&self, painter: &Painter, projection: &MapProjection) {
for element in &self.elements {
let screen_pos = projection.project(element.pos);
let uri = format!("bytes://{}.svg", rust_hash(&element.text));
match painter.ctx().try_load_texture(
&uri,
egui::TextureOptions::default(),
Default::default(),
) {
Ok(egui::load::TexturePoll::Ready { texture }) => {
let mut size = texture.size;
if element.scalable {
let scale = 2.0_f32.powi(i32::from(projection.zoom) - 10);
size *= scale;
}
let rect = egui::Rect::from_min_size(
screen_pos - size * element.anchor.to_vec2(),
size,
);
painter.image(
texture.id,
rect,
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
Color32::WHITE,
);
}
_ => {
painter.ctx().request_repaint();
}
}
}
}
}
fn rust_hash(s: &str) -> u64 {
let mut hasher = DefaultHasher::new();
s.hash(&mut hasher);
hasher.finish()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn svg_layer_serde() {
let mut layer = SvgLayer::default();
layer.add_element(SvgElement {
pos: GeoPos { lon: 1.0, lat: 2.0 },
text: "<svg></svg>".to_string(),
metadata: "test metadata".to_string(),
scalable: false,
clickable: true,
draggable: false,
anchor: Pos2::new(0.5, 0.5),
});
let json = serde_json::to_string(&layer).unwrap();
let deserialized: SvgLayer = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.elements.len(), 1);
assert_eq!(deserialized.elements[0].text, "<svg></svg>");
assert_eq!(deserialized.elements[0].metadata, "test metadata");
assert_eq!(deserialized.elements[0].pos, GeoPos { lon: 1.0, lat: 2.0 });
assert!(deserialized.elements[0].clickable);
assert!(!deserialized.elements[0].draggable);
}
#[test]
fn svg_layer_serde_backward_compatibility() {
let json = r#"{
"elements": [
{
"pos": {"lon": 1.0, "lat": 2.0},
"text": "<svg></svg>",
"metadata": "test metadata",
"scalable": false
}
]
}"#;
let deserialized: SvgLayer = serde_json::from_str(json).unwrap();
assert!(deserialized.elements[0].clickable);
assert!(!deserialized.elements[0].draggable);
}
#[test]
fn svg_layer_clickable_false() {
let mut layer = SvgLayer::default();
layer.add_element(SvgElement {
pos: GeoPos { lon: 1.0, lat: 2.0 },
text: "<svg></svg>".to_string(),
metadata: "test metadata".to_string(),
scalable: false,
clickable: false,
draggable: false,
anchor: default_anchor(),
});
let json = serde_json::to_string(&layer).unwrap();
let deserialized: SvgLayer = serde_json::from_str(&json).unwrap();
assert!(!deserialized.elements[0].clickable);
}
#[test]
fn svg_layer_draggable_true() {
let mut layer = SvgLayer::default();
layer.add_element(SvgElement {
pos: GeoPos { lon: 1.0, lat: 2.0 },
text: "<svg></svg>".to_string(),
metadata: "test metadata".to_string(),
scalable: false,
clickable: false,
draggable: true,
anchor: default_anchor(),
});
let json = serde_json::to_string(&layer).unwrap();
let deserialized: SvgLayer = serde_json::from_str(&json).unwrap();
assert!(deserialized.elements[0].draggable);
}
}