Open alexkirsz opened 9 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?
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).
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:
FeatureLayer
contains a set of features with their geometries. These are points in your case.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.
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.
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".
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!
@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![],
}
}
}
thanks for this @alexkirsz !
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.