Maximkaaa / galileo

General purpose cross-platform GIS-rendering library written in Rust
Apache License 2.0
367 stars 27 forks source link

Support Arc primitive for rendering #41

Open alexkirsz opened 9 months ago

alexkirsz commented 9 months ago

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 commented 8 months ago

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 commented 8 months ago

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 commented 8 months ago

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 commented 8 months ago

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 commented 8 months ago

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".

lennart commented 6 months ago

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 commented 6 months ago

@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 commented 5 months ago

thanks for this @alexkirsz !