diff --git a/galileo/examples/README.md b/galileo/examples/README.md index 1e3451f..60c3f0b 100644 --- a/galileo/examples/README.md +++ b/galileo/examples/README.md @@ -184,6 +184,24 @@ You can generate an image yourself running this example - Same as the raster tiles example, but with support for [egui](https://www.egui.rs/). + + + + + +[highlight_features](./highlight_features.rs) + + + + +![i](https://maximkaaa.github.io/galileo/highlight_features.png) + + + + +- Get and update features on cursor hover +- Show a different pin image based on feature state + diff --git a/galileo/examples/data/pin-green.png b/galileo/examples/data/pin-green.png new file mode 100644 index 0000000..594c287 Binary files /dev/null and b/galileo/examples/data/pin-green.png differ diff --git a/galileo/examples/highlight_features.rs b/galileo/examples/highlight_features.rs new file mode 100644 index 0000000..9fba496 --- /dev/null +++ b/galileo/examples/highlight_features.rs @@ -0,0 +1,181 @@ +use std::sync::{Arc, RwLock}; + +use galileo::control::{EventPropagation, UserEvent}; +use galileo::decoded_image::DecodedImage; +use galileo::layer::feature_layer::symbol::Symbol; +use galileo::layer::feature_layer::{Feature, FeatureLayer}; +use galileo::render::point_paint::PointPaint; +use galileo::render::render_bundle::RenderPrimitive; +use galileo::tile_scheme::TileSchema; +use galileo::MapBuilder; +use galileo_types::cartesian::{CartesianPoint3d, Point2d}; +use galileo_types::geo::{Crs, Projection}; +use galileo_types::geometry::Geom; +use galileo_types::impls::{Contour, Polygon}; +use galileo_types::{latlon, CartesianGeometry2d, Geometry}; +use lazy_static::lazy_static; +use nalgebra::Vector2; +use num_traits::AsPrimitive; + +const YELLOW_PIN: &[u8] = include_bytes!("data/pin-yellow.png"); +const GREEN_PIN: &[u8] = include_bytes!("data/pin-green.png"); + +lazy_static! { + static ref YELLOW_PIN_IMAGE: Arc = + Arc::new(DecodedImage::new(YELLOW_PIN).expect("Must have Yellow Pin Image")); + static ref GREEN_PIN_IMAGE: Arc = + Arc::new(DecodedImage::new(GREEN_PIN).expect("Must have Green Pin Image")); +} + +#[tokio::main] +async fn main() { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + run(MapBuilder::new()).await; +} + +#[derive(Debug, PartialEq, Default)] +pub(crate) struct PointMarker { + pub(crate) point: Point2d, + pub(crate) highlighted: bool, +} + +impl Feature for PointMarker { + type Geom = Self; + + fn geometry(&self) -> &Self::Geom { + self + } +} + +impl Geometry for PointMarker { + type Point = Point2d; + + fn project + ?Sized>( + &self, + projection: &P, + ) -> Option> { + self.point.project(projection) + } +} + +impl CartesianGeometry2d for PointMarker { + fn is_point_inside< + Other: galileo_types::cartesian::CartesianPoint2d< + Num = ::Num, + >, + >( + &self, + point: &Other, + tolerance: ::Num, + ) -> bool { + self.point.is_point_inside(point, tolerance) + } + + fn bounding_rectangle( + &self, + ) -> Option< + galileo_types::cartesian::Rect< + ::Num, + >, + > { + None + } +} + +struct ColoredPointSymbol {} + +pub async fn run(builder: MapBuilder) { + #[cfg(not(target_arch = "wasm32"))] + let builder = builder.with_raster_tiles( + |index| { + format!( + "https://tile.openstreetmap.org/{}/{}/{}.png", + index.z, index.x, index.y + ) + }, + TileSchema::web(18), + ); + + let projection = Crs::EPSG3857 + .get_projection() + .expect("must find projection"); + + let feature_layer = FeatureLayer::new( + [ + latlon!(53.732562, -1.863383), + latlon!(53.728265, -1.839966), + latlon!(53.704014, -1.786128), + ] + .iter() + .map(|point| PointMarker { + point: projection.project(point).unwrap(), + ..Default::default() + }) + .collect(), + ColoredPointSymbol {}, + Crs::EPSG3857, + ); + + let feature_layer = Arc::new(RwLock::new(feature_layer)); + + builder + .center(latlon!(53.732562, -1.863383)) + .resolution(30.0) + .with_layer(feature_layer.clone()) + .with_event_handler(move |ev, map| { + if let UserEvent::PointerMoved(event) = ev { + let mut layer = feature_layer.write().unwrap(); + + let Some(position) = map.view().screen_to_map(event.screen_pointer_position) else { + return EventPropagation::Stop; + }; + + for mut feature_container in layer.features_mut().iter_mut() { + feature_container.as_mut().highlighted = false; + } + + for mut feature_container in + layer.get_features_at_mut(&position, map.view().resolution() * 20.0) + { + let state = feature_container.as_ref().highlighted; + feature_container.as_mut().highlighted = !state; + } + + map.redraw(); + } + galileo::control::EventPropagation::Propagate + }) + .build() + .await + .run(); +} + +impl Symbol for ColoredPointSymbol { + fn render<'a, N, P>( + &self, + feature: &PointMarker, + geometry: &'a Geom

, + _min_resolution: f64, + ) -> Vec, Polygon

>> + where + N: AsPrimitive, + P: CartesianPoint3d + Clone, + { + if let Geom::Point(point) = geometry { + vec![RenderPrimitive::new_point_ref( + point, + PointPaint::image( + if feature.highlighted { + GREEN_PIN_IMAGE.clone() + } else { + YELLOW_PIN_IMAGE.clone() + }, + Vector2::new(0.5, 0.5), + 1.0, + ), + )] + } else { + vec![] + } + } +} diff --git a/galileo/src/render/render_bundle/tessellating.rs b/galileo/src/render/render_bundle/tessellating.rs index 0d18909..35920ad 100644 --- a/galileo/src/render/render_bundle/tessellating.rs +++ b/galileo/src/render/render_bundle/tessellating.rs @@ -31,14 +31,28 @@ pub(crate) struct TessellatingRenderBundle { pub poly_tessellation: VertexBuffers, pub points: Vec, pub screen_ref: ScreenRefTessellation, - pub images: Vec<(usize, [ImageVertex; 4])>, + pub images: Vec, pub clip_area: Option>, - pub image_store: Vec>, + pub image_store: Vec, pub primitives: Vec, vacant_ids: Vec, + vacant_image_ids: Vec, + vacant_image_store_ids: Vec, buffer_size: usize, } +#[derive(Debug, Clone)] +pub(crate) enum ImageStoreInfo { + Vacant, + Image(Arc), +} + +#[derive(Debug, Clone)] +pub(crate) enum ImageInfo { + Vacant, + Image((usize, [ImageVertex; 4])), +} + pub(crate) type ScreenRefTessellation = VertexBuffers; #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] @@ -75,6 +89,8 @@ impl TessellatingRenderBundle { clip_area: None, image_store: Vec::new(), vacant_ids: vec![], + vacant_image_ids: vec![], + vacant_image_store_ids: vec![], buffer_size: 0, } } @@ -112,40 +128,38 @@ impl TessellatingRenderBundle { paint: ImagePaint, ) -> PrimitiveId { let opacity = paint.opacity as f32 / 255.0; - let image_index = self.images.len(); self.buffer_size += image.bytes.len() + std::mem::size_of::() * 4; let index = self.add_image_to_store(Arc::new(image)); - self.images.push(( - index, - [ - ImageVertex { - position: [vertices[0].x() as f32, vertices[0].y() as f32], - opacity, - tex_coords: [0.0, 1.0], - offset: [0.0, 0.0], - }, - ImageVertex { - position: [vertices[1].x() as f32, vertices[1].y() as f32], - opacity, - tex_coords: [0.0, 0.0], - offset: [0.0, 0.0], - }, - ImageVertex { - position: [vertices[3].x() as f32, vertices[3].y() as f32], - opacity, - tex_coords: [1.0, 1.0], - offset: [0.0, 0.0], - }, - ImageVertex { - position: [vertices[2].x() as f32, vertices[2].y() as f32], - opacity, - tex_coords: [1.0, 0.0], - offset: [0.0, 0.0], - }, - ], - )); + let vertices = [ + ImageVertex { + position: [vertices[0].x() as f32, vertices[0].y() as f32], + opacity, + tex_coords: [0.0, 1.0], + offset: [0.0, 0.0], + }, + ImageVertex { + position: [vertices[1].x() as f32, vertices[1].y() as f32], + opacity, + tex_coords: [0.0, 0.0], + offset: [0.0, 0.0], + }, + ImageVertex { + position: [vertices[3].x() as f32, vertices[3].y() as f32], + opacity, + tex_coords: [1.0, 1.0], + offset: [0.0, 0.0], + }, + ImageVertex { + position: [vertices[2].x() as f32, vertices[2].y() as f32], + opacity, + tex_coords: [1.0, 0.0], + offset: [0.0, 0.0], + }, + ]; + + let image_index = self.add_image_info(index, vertices); let id = self.primitives.len(); @@ -167,7 +181,6 @@ impl TessellatingRenderBundle { P: CartesianPoint3d, { let opacity = opacity as f32 / 255.0; - let image_index = self.images.len(); self.buffer_size += image.bytes.len() + size_of::() * 4; @@ -176,39 +189,50 @@ impl TessellatingRenderBundle { let offset_y = offset[1] * height; let index = self.add_image_to_store(image); - self.images.push(( - index, - [ - ImageVertex { - position, - opacity, - tex_coords: [0.0, 1.0], - offset: [offset_x, offset_y - height], - }, - ImageVertex { - position, - opacity, - tex_coords: [0.0, 0.0], - offset: [offset_x, offset_y], - }, - ImageVertex { - position, - opacity, - tex_coords: [1.0, 1.0], - offset: [offset_x + width, offset_y - height], - }, - ImageVertex { - position, - opacity, - tex_coords: [1.0, 0.0], - offset: [offset_x + width, offset_y], - }, - ], - )); + let vertices = [ + ImageVertex { + position, + opacity, + tex_coords: [0.0, 1.0], + offset: [offset_x, offset_y - height], + }, + ImageVertex { + position, + opacity, + tex_coords: [0.0, 0.0], + offset: [offset_x, offset_y], + }, + ImageVertex { + position, + opacity, + tex_coords: [1.0, 1.0], + offset: [offset_x + width, offset_y - height], + }, + ImageVertex { + position, + opacity, + tex_coords: [1.0, 0.0], + offset: [offset_x + width, offset_y], + }, + ]; + + let image_index = self.add_image_info(index, vertices); PrimitiveInfo::Image { image_index } } + fn add_image_info(&mut self, image_store_index: usize, vertices: [ImageVertex; 4]) -> usize { + if let Some(id) = self.vacant_image_ids.pop() { + self.images[id] = ImageInfo::Image((image_store_index, vertices)); + id + } else { + let index = self.images.len(); + self.images + .push(ImageInfo::Image((image_store_index, vertices))); + index + } + } + fn add_primitive_info(&mut self, info: PrimitiveInfo) -> PrimitiveId { if let Some(id) = self.vacant_ids.pop() { self.primitives[id] = info; @@ -222,14 +246,24 @@ impl TessellatingRenderBundle { fn add_image_to_store(&mut self, image: Arc) -> usize { for (i, stored) in self.image_store.iter().enumerate() { - if Arc::ptr_eq(stored, &image) { - return i; + match stored { + ImageStoreInfo::Vacant => {} + ImageStoreInfo::Image(stored) => { + if Arc::ptr_eq(stored, &image) { + return i; + } + } } } - let index = self.image_store.len(); - self.image_store.push(image); - index + if let Some(id) = self.vacant_image_store_ids.pop() { + self.image_store[id] = ImageStoreInfo::Image(image); + id + } else { + let index = self.image_store.len(); + self.image_store.push(ImageStoreInfo::Image(image)); + index + } } pub fn add( @@ -306,10 +340,36 @@ impl TessellatingRenderBundle { if index >= self.images.len() { Err(GalileoError::Generic("index out of bounds".into())) } else { - let (image_id, _) = self.images.remove(index); - let image = self.image_store.remove(image_id); + let image_id = match std::mem::replace(&mut self.images[index], ImageInfo::Vacant) { + ImageInfo::Vacant => { + // this should not happen + return Err(GalileoError::Generic( + "tried to replace vacant image with vacant slot".into(), + )); + } + ImageInfo::Image((image_id, _)) => { + self.vacant_image_ids.push(index); + image_id + } + }; + + let stored_image_unused = self.images.iter().all(|info| match info { + ImageInfo::Vacant => false, + ImageInfo::Image((i, _)) => *i != image_id, + }); - self.buffer_size -= image.bytes.len() + size_of::() * 4; + if stored_image_unused { + match std::mem::replace(&mut self.image_store[image_id], ImageStoreInfo::Vacant) { + ImageStoreInfo::Vacant => { + // this should not happen + } + ImageStoreInfo::Image(image) => { + self.vacant_image_store_ids.push(image_id); + + self.buffer_size -= image.bytes.len() + size_of::() * 4; + } + } + } for info in &mut self.primitives { match info { @@ -607,12 +667,19 @@ impl TessellatingRenderBundle { .ok_or(GalileoError::Generic("primitive does not exist".into()))?; match info { PrimitiveInfo::Image { image_index } => { - let (_, vertices) = self + match self .images .get_mut(*image_index) - .ok_or(GalileoError::Generic("invalid image id".into()))?; - for vertex in vertices { - vertex.opacity = paint.opacity as f32 / 255.0; + .ok_or(GalileoError::Generic("invalid image id".into()))? + { + ImageInfo::Vacant => { + return Err(GalileoError::Generic("tried to modify vacant image".into())) + } + ImageInfo::Image((_, vertices)) => { + for vertex in vertices { + vertex.opacity = paint.opacity as f32 / 255.0; + } + } } } _ => return Err(GalileoError::Generic("invalid primitive type".into())), @@ -913,19 +980,27 @@ impl TessellatingRenderBundle { let Some(transform) = view.map_to_scene_transform() else { return; }; - self.images.sort_by(|(_, vertex_set_a), (_, vertex_set_b)| { - let point_a = Point3d::new( - vertex_set_a[0].position[0] as f64, - vertex_set_a[0].position[1] as f64, - 0.0, - ) - .to_homogeneous(); - let point_b = Point3d::new( - vertex_set_b[0].position[0] as f64, - vertex_set_b[0].position[1] as f64, - 0.0, - ) - .to_homogeneous(); + self.images.sort_by(|info_a, info_b| { + let point_a = match info_a { + ImageInfo::Vacant => Point3d::new(0.0, 0.0, 0.0).to_homogeneous(), + ImageInfo::Image((_, vertex_set_a)) => Point3d::new( + vertex_set_a[0].position[0] as f64, + vertex_set_a[0].position[1] as f64, + 0.0, + ) + .to_homogeneous(), + }; + + let point_b = match info_b { + ImageInfo::Vacant => Point3d::new(0.0, 0.0, 0.0).to_homogeneous(), + ImageInfo::Image((_, vertex_set_b)) => Point3d::new( + vertex_set_b[0].position[0] as f64, + vertex_set_b[0].position[1] as f64, + 0.0, + ) + .to_homogeneous(), + }; + let projected_a = transform * point_a; let projected_b = transform * point_b; diff --git a/galileo/src/render/render_bundle/tessellating/serialization.rs b/galileo/src/render/render_bundle/tessellating/serialization.rs index 0aed90c..f2619b9 100644 --- a/galileo/src/render/render_bundle/tessellating/serialization.rs +++ b/galileo/src/render/render_bundle/tessellating/serialization.rs @@ -1,6 +1,6 @@ use crate::decoded_image::DecodedImage; use crate::render::render_bundle::tessellating::{ - PolyVertex, PrimitiveInfo, ScreenRefVertex, TessellatingRenderBundle, + ImageInfo, ImageStoreInfo, PolyVertex, PrimitiveInfo, ScreenRefVertex, TessellatingRenderBundle, }; use lyon::lyon_tessellation::VertexBuffers; use serde::{Deserialize, Serialize}; @@ -12,9 +12,11 @@ pub(crate) struct TessellatingRenderBundleBytes { pub poly_tessellation: PolyVertexBuffersBytes, pub points: Vec, pub screen_ref: ScreenRefVertexBuffersBytes, - pub images: Vec, + pub images: Vec>, pub primitives: Vec, - pub image_store: Vec<(u32, u32, Vec)>, + pub image_store: Vec)>>, + pub vacant_image_ids: Vec, + pub vacant_image_store_ids: Vec, pub clip_area: Option, pub bundle_size: usize, } @@ -89,17 +91,27 @@ impl TessellatingRenderBundle { images: self .images .into_iter() - .map(|(image_index, vertices)| ImageBytes { - image_index, - vertices: bytemuck::cast_vec(vertices.to_vec()), + .map(|image_info| match image_info { + ImageInfo::Vacant => None, + ImageInfo::Image((image_index, vertices)) => Some(ImageBytes { + image_index, + vertices: bytemuck::cast_vec(vertices.to_vec()), + }), }) .collect(), primitives: self.primitives, image_store: self .image_store .into_iter() - .map(|image| (image.dimensions.0, image.dimensions.1, image.bytes.clone())) + .map(|image_info| match image_info { + ImageStoreInfo::Vacant => None, + ImageStoreInfo::Image(image) => { + Some((image.dimensions.0, image.dimensions.1, image.bytes.clone())) + } + }) .collect(), + vacant_image_ids: self.vacant_image_ids, + vacant_image_store_ids: self.vacant_image_store_ids, clip_area: self.clip_area.map(|v| v.into()), bundle_size: self.buffer_size, }; @@ -115,30 +127,34 @@ impl TessellatingRenderBundle { images: bundle .images .into_iter() - .map( - |ImageBytes { - image_index, - vertices, - }| { + .map(|item| match item { + Some(ImageBytes { + image_index, + vertices, + }) => { let vertices = bytemuck::cast_vec(vertices) .try_into() .expect("invalid vector length"); - (image_index, vertices) - }, - ) + ImageInfo::Image((image_index, vertices)) + } + None => ImageInfo::Vacant, + }) .collect(), primitives: bundle.primitives, image_store: bundle .image_store .into_iter() - .map(|(width, height, bytes)| { - Arc::new(DecodedImage { + .map(|stored| match stored { + Some((width, height, bytes)) => ImageStoreInfo::Image(Arc::new(DecodedImage { bytes, dimensions: (width, height), - }) + })), + None => ImageStoreInfo::Vacant, }) .collect(), + vacant_image_ids: bundle.vacant_image_ids, + vacant_image_store_ids: bundle.vacant_image_store_ids, clip_area: bundle.clip_area.map(|v| v.into_typed_unchecked()), buffer_size: bundle.bundle_size, vacant_ids: vec![], diff --git a/galileo/src/render/wgpu/mod.rs b/galileo/src/render/wgpu/mod.rs index 65ec86c..3efc3c2 100644 --- a/galileo/src/render/wgpu/mod.rs +++ b/galileo/src/render/wgpu/mod.rs @@ -26,6 +26,7 @@ use crate::render::wgpu::pipelines::Pipelines; use crate::view::MapView; use crate::Color; +use super::render_bundle::tessellating::{ImageInfo, ImageStoreInfo}; use super::{Canvas, PackedBundle, RenderOptions}; mod pipelines; @@ -862,23 +863,35 @@ impl WgpuPackedBundle { let textures: Vec<_> = image_store .iter() - .map(|decoded_image| { - render_set.pipelines.image_pipeline().create_image_texture( - &renderer.device, - &renderer.queue, - decoded_image, - ) + .map(|stored| match stored { + ImageStoreInfo::Vacant => None, + ImageStoreInfo::Image(decoded_image) => { + Some(render_set.pipelines.image_pipeline().create_image_texture( + &renderer.device, + &renderer.queue, + decoded_image, + )) + } }) .collect(); let mut image_buffers = vec![]; - for (image_index, vertices) in images { - let image = render_set.pipelines.image_pipeline().create_image( - &renderer.device, - textures[*image_index].clone(), - vertices, - ); - image_buffers.push(image); + for image_info in images { + if let ImageInfo::Image((image_index, vertices)) = image_info { + let image = render_set.pipelines.image_pipeline().create_image( + &renderer.device, + textures + .get(*image_index) + .expect("texture at index must exist") + .clone() + .expect("image texture must not be None") + .clone(), + vertices, + ); + image_buffers.push(image); + } else { + // ignore vacant image slots + } } Self {