Skip to content

Keep shapely for polygon containment only and drop matplotlib - #42

Open
amorfinv wants to merge 3 commits into
mainfrom
feat/shapely-areafilter
Open

Keep shapely for polygon containment only and drop matplotlib#42
amorfinv wants to merge 3 commits into
mainfrom
feat/shapely-areafilter

Conversation

@amorfinv

@amorfinv amorfinv commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Closes #32

matplotlib was a dependency with just a single geometric predicate: the point-in-polygon test in Poly.checkInside. This PR unifies all area shapes on shapely geometries instead, so containment is one vectorized shapely.contains_xy call in the Shape base class, and matplotlib is dropped from the dependencies (it remains in the environment only as a transitive dependency of openap).

Each shape now builds a shapely geometry in (lat, lon) space at construction: Box via shapely.box on its sorted corners, Poly directly from its vertex list, and Circle as a geographic N-gon whose vertices are placed with kwikpos, keeping the cos(lat) longitude scaling correct. The circle's vertex count adapts to its radius so the polygon border stays within 0.05 NM of the true circle at any size, clamped to 36–720 vertices. Line keeps an explicit all-False checkInside since it has zero area.

This PR swaps matplotlib for shapely as the point-in-polygon backend: Poly now holds a private shapely.Polygon and uses contains_xy instead of a matplotlib.path.Path, which lets matplotlib come out of the dependencies.

Box and Circle keep their existing geographic math unchanged (bounds comparison and kwikdist), since shapely's flat-plane assumption breaks near the poles and at the antimeridian. Polygons that hit those cases, crossing the 180° line or winding around a pole, are now rejected at definition time with an error rather than silently returning wrong answers.

The Shape base class is removed in favor of a HasArea protocol implemented only by Box, Circle, and Poly, so Line no longer carries a checkInside method for a question it can't meaningfully answer; this drops the public Shape, .raw, and .kind(), which nothing in the repo used.

Every Shape now carries a shapely geometry in (lat, lon) space and
containment is one vectorized contains_xy call in the base class.
Circles become geographic N-gons via kwikpos, with the vertex count
sized to keep the border within 0.05 NM of the true circle.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 3, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
minisky 46544fc Aug 09 2026, 06:27 AM

altitude bounds [m].
"""

# Maximum distance [nm] between the polygon border and the true circle

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abc8747

I am bit unsure about approaching circles this way.

One option is to just buffer a point (https://shapely.readthedocs.io/en/stable/reference/shapely.buffer.html) into a LinearRing. But I think for large radius, it will look like an eclipse

Another option could be to just drop the circle shape as it is similar to polygon. Users can already approximate a circle with a poly command.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I'm not sure we need to use shapely here, the old implementation of kwikdist looks alright to me.

Maybe switch to using qdrdist or latlondist for large radius or near the poles?

@abc8747 abc8747 Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I see that you now require all base classes of Shape to provide a shapely geometry. I think if we don't intend to use the shapely geometry elsewhere in the code we can just define a protected _geom (for Polygon) and not define it at all for other shapes

@amorfinv amorfinv Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking for an urban airspace plugin with many geofences it will be nice if shapes are already in shapely to be able to use rtree for quick spatial operations. perhaps plugins can make their own shapes though.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but the reason why I didn't want to force every shape to own a shapely geometry is more to do with the fact that shapely itself only works with planar Cartesian geometry. With the current latitude/longitude representation, we need to be very careful about two cases:

  • polygon with vertices (10, 170), (10, -170), (-10, -170), and (-10, 170) that encloses the antimeridian. if we switch to using shapely.box, (0, 0) is considered inside the polygon, but should be outside.
  • a 100 NM circle centred at the north pole. right now the new implementation generates vertices above 90° latitude and produces an invalid shapely polygon.

I think we should avoid making the planar shapely geometry as a core invariant, and avoid exposing it in general to prevent misuse. For:

  • box and circle: I actually prefer the old implementation, which should solve the two issues
  • polygon: I think we can still use shapely's contains_xy, but keep it protected. for the antimeridian case we should follow RFC 7946 §3.1.9 to split the polygon into two chunks internally. for the polygons that enclose the poles, I can't think of a good solution so I'm geared towards rejecting it outright

Re shapely's STRTree: I haven't used it so I can't comment on whether it will bring any performance improvement (I suspect since we are dealing with a relatively small number of aircraft and geofences, it may not be necessary)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have ran simulation with a large amount of aircraft and geofences where it is beneficial to have the Rtree. However, perhaps that can be left for a plugin.

I will work on changing the scope of this PR sometime next week

super().__init__(name, coordinates)
self.geom = shapely.LineString(np.reshape(coordinates, (-1, 2)))

def checkInside(self, lat: np.ndarray, lon: np.ndarray, alt: np.ndarray) -> np.ndarray:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit, but it is category error to check whether an aircraft is inside a line. To prevent this we should remove the Shape base class altogether to avoid implementation inheritance in general. We could use explicit structural subtyping with Protocols, something like:

class HasArea(Protocol):
    def contains(self, lat, lon, alt):
        ...

class Box(HasArea): ...
class Circle(HasArea): ...
class Polygon(HasArea): ...
# but not for line.

@amorfinv amorfinv changed the title Unify area shapes on shapely and drop matplotlib Keep shapely for polygon containment only and drop matplotlib Aug 9, 2026
@amorfinv

amorfinv commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

I have managed to rescope this PR to keep most of the original functionality and only use shapely for contains inside Polygon

return self.define_area(name, "LINE", coords)

def checkInside(
def contains(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't taken a look at what parts of the code uses AreaFilter but it looks like we are still associating a "line" (which has no area) as part of an "area filter". I would get rid of Line from AreaFilter.shapes and let callers handle it. Maybe that is too big of a refactor so I would just merge it

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll give it a check later today but these shapes are mostly helpers for the user. I don't think they are that important for calculation. I can rename this to shapes or something

I recall that in bluesky there is a plugin which uses areafilter to create an experiment area. So basically anything outside tje shape is not logged and quickly deleted.

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

Successfully merging this pull request may close these issues.

Unify area shapes on shapely and drop matplotlib (point-in-polygon)

2 participants