Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Arc primitive for rendering #41

Open
alexkirsz opened this issue Feb 4, 2024 · 8 comments
Open

Support Arc primitive for rendering #41

alexkirsz opened this issue Feb 4, 2024 · 8 comments

Comments

@alexkirsz
Copy link
Contributor

Hey!

I'd like to draw a circle with a radius that's resolution-dependent (i.e. specified in meters, instead of view units). That breaks down at small resolutions because of projection, but my use case is for radii that are < 10km.

A workaround right now is to draw a circle-like polygon.

@Maximkaaa
Copy link
Owner

Well, any resolution-dependent circle would be interpolated as a polygon anyway. I guess what you mean is that you want the circle be interpolated with different precision based on current map resolution. Is that right?

@alexkirsz
Copy link
Contributor Author

No, I meant that I’d like the circle to have different dimensions at different levels of zoom. Currently, PointPaint::circle will create a circle that stays the same size no matter the zoom level. Instead, I’d like the circle to scale at the same rate as the map (hence meters for the unit instead of pixels/points).

@Maximkaaa
Copy link
Owner

So, you want to draw a circle on the map around a given point with fixed real-world radius. Something like a zone with radius of 1km. When rendered to the screen, that circle will be approximated as a polygon anyways, that's what I meant.

Currently, feature layers are not documented, but the basic Idea behind them is that:

  1. FeatureLayer contains a set of features with their geometries. These are points in your case.
  2. Then the layer calls a symbol to convert that geometry into a set of primitives to draw. On this step a point can be converted into a circle with fixed screen size, an image or, in your case, into a polygon with given map radius.
  3. Then the renderer draws the primitives.

So to achieve what you want, you don't need to change your features to be polygons instead of points, instead you can create a symbol that will draw them as polygons with given parameters. For example, the symbol can take some radius attribute from the feature, take point geometry position and create a polygon around that position with the given radius.

There is also a notion of LODs (levels of detail) in feature layers that allow you to draw your features with different precision based on resolution (see feature_layers example). Symbol also gets this info through min_resolution parameter, so you can write your symbol so that the approximating polygons have different number of vertices based on resolution, to make them always smooth.

In future, I'm planning to add Arc primitive, so that you wouldn't have to approximate your circle by hand. It will also automatically apply LOD information. And also there will be a DynamicFeatureLayer that will not cache tessellation of polygons between redraws, so you won't have to think about LODs at all if you have small enough number of geometries in your layer.

@alexkirsz
Copy link
Contributor Author

Considering my use case is for circles of radius < 10km, with no regards for projection, I thought it might be more performant to use a signed distance function for rendering, instead of going through the tessellation pipeline.

But for now, I'll draw them as polygons as you suggested.

@Maximkaaa
Copy link
Owner

That's true, just creating a circle without tessellation would be more efficient, but for simple shapes like circles it shouldn't really matter. Anyway, adding Arc primitive would solve that also, as it will do exactly that. So, I guess, we can rename this issue into "Support Arc primitive for rendering".

@alexkirsz alexkirsz changed the title Resolution-dependent circle radius Support Arc primitive for rendering Feb 5, 2024
@lennart
Copy link
Contributor

lennart commented Apr 14, 2024

I tried to implement the suggestion to have a symbol render a polygon from a point with a radius but failed to satisfy the constraints on the render function of the trait. I may be missing something obvious so here is the current implementation that does not compile:

impl Symbol<Spot> for SpotSymbol {
    #[allow(clippy::cast_possible_truncation)]
    #[allow(clippy::cast_precision_loss)]
    fn render<'a, N, P>(
        &self,
        feature: &Spot,
        geometry: &'a Geom<P>,
        min_resolution: f64,
    ) -> Vec<
        galileo::render::render_bundle::RenderPrimitive<
            'a,
            N,
            P,
            galileo_types::impls::Contour<P>,
            galileo_types::impls::Polygon<P>,
        >,
    >
    where
        N: AsPrimitive<f32>,
        P: galileo_types::cartesian::CartesianPoint3d<Num = N> + Clone,
    {
        match geometry {
            Geom::Point(point) => {
                let mut render_primitives = vec![];
                let color = Color::rgba(0, 0, 255, 128);
                let circle_subdivision = 25;
                let mut points = vec![];
                let mut i: f64 = 0.0;
                let step_size = std::f64::consts::PI / f64::from(circle_subdivision);
                let radius = feature.radius;

                while i < std::f64::consts::PI * 2.0 {
                    let x = i.sin();
                    let y = i.cos();
                    let new_pos = Point3::new(
                        point.x().as_() + (x as f32 * radius as f32),
                        point.y().as_() + (y as f32 * radius as f32),
                        point.z().as_(),
                    );
                    points.push(new_pos);
                    i += step_size;
                }

                let contour = ClosedContour::new(points);
                let poly = Polygon::new(contour, vec![]);

                render_primitives.push(RenderPrimitive::new_polygon(poly, PolygonPaint { color }));

                render_primitives
            }
            _ => vec![],
        }
    }
}

with the error:

error[E0308]: mismatched types
   --> editor/src/state/galileo.rs:387:17
    |
329 |       fn render<'a, N, P>(
    |                        - expected this type parameter
...
334 |       ) -> Vec<
    |  __________-
335 | |         galileo::render::render_bundle::RenderPrimitive<
336 | |             'a,
337 | |             N,
...   |
341 | |         >,
342 | |     >
    | |_____- expected `std::vec::Vec<RenderPrimitive<'a, N, P, galileo_types::impls::Contour<P>, galileo_types::impls::Polygon<P>>>` because of return type
...
380 |                   render_primitives.push(RenderPrimitive::new_polygon(poly, PolygonPaint { color }));
    |                   -----------------      ---------------------------------------------------------- this argument has type `RenderPrimitive<'_, f32, OPoint<f32, Const<3>>, _, galileo_types::impls::Polygon<OPoint<f32, Const<3>>>>`...
    |                   |
    |                   ... which causes `render_primitives` to have type `std::vec::Vec<RenderPrimitive<'_, f32, OPoint<f32, Const<3>>, _, galileo_types::impls::Polygon<OPoint<f32, Const<3>>>>>`
...
387 |                   render_primitives
    |                   ^^^^^^^^^^^^^^^^^ expected `Vec<RenderPrimitive<'_, N, P, Contour<P>, Polygon<P>>>`, found `Vec<RenderPrimitive<'_, f32, OPoint<f32, Const<3>>, _, ...>>`
    |
    = note: expected struct `std::vec::Vec<RenderPrimitive<'a, N, P, galileo_types::impls::Contour<P>, galileo_types::impls::Polygon<P>>>`
               found struct `std::vec::Vec<RenderPrimitive<'_, f32, OPoint<f32, Const<3>>, _, galileo_types::impls::Polygon<OPoint<f32, Const<3>>>>>`

I am guessing, that I should not create a new Point3 manually from the components of the geometry but rather construct new points with the same type as the geometry has (P), however I did not manage to do that...

I also tried to implement the polygon construction within the feature itself but I am unsure if this is the right way to do (Code compiled but the radius of the resulting circle was off, which may be a different problem with projection etc.)

Is there any example code for the symbol rendering implementation for manual construction of shapes? Within the examples of the repository I could only find parts where the geometry that is rendered is already present in the given feature.

Thanks in advance for any advice!

@alexkirsz
Copy link
Contributor Author

alexkirsz commented Apr 22, 2024

@lennart This is what I have:

struct PositionGeometry {
    geometry: Polygon<Point2d>,
}

impl galileo_types::Geometry for PositionGeometry {
    type Point = Point2d;

    fn project<Proj>(&self, projection: &Proj) -> Option<Geom<Proj::OutPoint>>
    where
        Proj: galileo_types::geo::Projection<InPoint = Self::Point> + ?Sized,
    {
        self.geometry.project(projection)
    }
}

impl CartesianGeometry2d<Point2d> for PositionGeometry {
    fn is_point_inside<Other: CartesianPoint2d<Num = f64>>(
        &self,
        point: &Other,
        tolerance: f64,
    ) -> bool {
        // TODO(alexkirsz) Quick check?

        self.geometry.is_point_inside(point, tolerance)
    }

    fn bounding_rectangle(&self) -> Option<Rect> {
        // TODO(alexkirsz)
        None
    }
}

impl Feature for PositionGeometry {
    type Geom = Self;

    fn geometry(&self) -> &Self::Geom {
        self
    }
}

fn generate_circle_polygon(
    point: GeoPoint2d,
    radius_meters: f64,
    num_points: usize,
) -> Vec<GeoPoint2d> {
    let earth_radius_meters = 6_371_000.0;
    let lat_rad = point.lat_rad();
    let lon_rad = point.lon_rad();
    let angular_distance = radius_meters / earth_radius_meters;

    let mut polygon_points = Vec::with_capacity(num_points);

    for i in 0..num_points {
        let bearing = 2.0 * std::f64::consts::PI * (i as f64) / (num_points as f64);
        let lat_point = (lat_rad.sin() * angular_distance.cos()
            + lat_rad.cos() * angular_distance.sin() * bearing.cos())
        .asin();
        let lon_point = lon_rad
            + (bearing.sin() * angular_distance.sin() * lat_rad.cos())
                .atan2(angular_distance.cos() - lat_rad.sin() * lat_point.sin());

        polygon_points.push(GeoPoint2d::latlon(
            lat_point.to_degrees(),
            lon_point.to_degrees(),
        ));
    }

    polygon_points
}

// then

features.insert(PositionGeometry {
    geometry: Polygon::new(
        ClosedContour::new(generate_circle_polygon(
            GeoPoint2d::latlon(
                lat,
                lon,
            ),
            radius,
            50,
        ))
        .project_points(&projection)
        .expect("projection failed"),
        vec![],
    ),
});

I then use a simple symbol to render theses:

#[derive(Debug, Copy, Clone)]
pub struct CircleSymbol {
    pub color: Color,
}

impl CircleSymbol {
    pub fn new(color: Color) -> Self {
        Self { color }
    }
}

impl<F> Symbol<F> for CircleSymbol {
    fn render<'a, N, P>(
        &self,
        _feature: &F,
        geometry: &'a Geom<P>,
        min_resolution: f64,
    ) -> Vec<RenderPrimitive<'a, N, P, Contour<P>, Polygon<P>>>
    where
        N: AsPrimitive<f32>,
        P: CartesianPoint3d<Num = N> + Clone,
    {
        match geometry {
            Geom::Polygon(polygon) => {
                vec![RenderPrimitive::new_polygon_ref(
                    polygon,
                    PolygonPaint { color: self.color },
                )]
            }
            _ => vec![],
        }
    }
}

@lennart
Copy link
Contributor

lennart commented May 5, 2024

thanks for this @alexkirsz !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants