Introduction
When working with geospatial data in Python, a common task is to test whether points fall inside a polygon.
For example, you may want to know:
- if a fire detection falls inside a known persistent thermal anomaly area;
- if a point intersects a solar farm polygon;
- if a point is outside the polygon but still inside a buffer zone;
- if random test points intersect a geometry before using the method on a larger dataset.
In this tutorial, we will use GeoPandas, Shapely, and Folium to test whether points intersect a polygon and a buffered polygon.
Table of contents
- Introduction
- Goal
- Required packages
- Define a polygon from coordinates
- Check the polygon coordinates
- Compute the polygon area in square kilometers
- Create manual test points
- Test whether points intersect the polygon
- Why use intersects?
- Create a buffer around the polygon
- Add a readable class column
- Generate random points around the polygon
- Test random points against the polygon and buffer
- Count points by class
- Spatial join alternative
- Export polygon, buffer, and points to GeoJSON
- Create a Leaflet map with Folium
- Save the map as HTML
- Create a standalone Leaflet page
- Run the Leaflet example locally
- Common mistake: creating buffers in latitude/longitude
- Conclusion
- References
We will use a polygon extracted from gdf_topaz, representing the largest polygon from a Topaz Solar Farm geometry. Then we will create points around it and add Boolean columns indicating whether each point intersects the polygon or its buffer.
Goal
The goal is to create a GeoDataFrame with points and add columns like this:
1 | point_id | longitude | latitude | intersects_polygon | intersects_buffer |
Where:
intersects_polygon = Truemeans the point intersects the original polygon.intersects_buffer = Truemeans the point intersects the polygon or the surrounding buffer zone.intersects_polygon = Falseandintersects_buffer = Truemeans the point is outside the polygon but close to it.intersects_buffer = Falsemeans the point is outside both the polygon and the buffer.
Required packages
1 2 3 4 5 6 7 8 | from pathlib import Path import numpy as np import pandas as pd import geopandas as gpd import folium from shapely.geometry import Point, Polygon |
If needed, install the packages with:
1 | # %pip install geopandas shapely pandas numpy folium mapclassify
|
Define a polygon from coordinates
The coordinates below are in longitude/latitude order.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | topaz_polygon_coordinates = [ (-120.028905, 35.360990), (-120.027158, 35.360990), (-120.027210, 35.353300), (-120.032477, 35.353315), (-120.032578, 35.359217), (-120.037711, 35.359246), (-120.037687, 35.368248), (-120.039398, 35.368279), (-120.039423, 35.378957), (-120.037763, 35.378977), (-120.037782, 35.380434), (-120.027230, 35.380497), (-120.027087, 35.363782), (-120.028902, 35.363782), (-120.028905, 35.360990), ] |
Make sure the polygon is closed. A closed polygon means the first and last coordinates are identical.
1 2 | if topaz_polygon_coordinates[0] != topaz_polygon_coordinates[-1]: topaz_polygon_coordinates.append(topaz_polygon_coordinates[0]) |
Now create a Shapely polygon:
1 | topaz_polygon = Polygon(topaz_polygon_coordinates) |
Then create a GeoDataFrame:
1 2 3 4 5 6 7 8 9 10 | gdf_topaz_polygon = gpd.GeoDataFrame( { "name": ["Topaz Solar Farm - extracted largest polygon"], "source": ["Largest polygon extracted from gdf_topaz"], }, geometry=[topaz_polygon], crs="EPSG:4326", ) gdf_topaz_polygon |
| name | source | geometry | area_km2 |
|---|---|---|---|
| Topaz Solar Farm - extracted largest polygon | Largest polygon extracted from gdf_topaz | POLYGON ((-120.028905 35.36099, -120.027158 35.36099, -120.02721 35.3533, -120.032477 35.353315, -120.032578 35.359217, -120.037711 35.359246, -120.037687 35.368248, -120.039398 35.368279, -120.039423 35.378957, -120.037763 35.378977, -120.037782 35.380434, -120.02723 35.380497, -120.027087 35.363782, -120.028902 35.363782, -120.028905 35.36099)) | 2.7147696427931787 |
The CRS is EPSG:4326, meaning the coordinates are longitude and latitude.
Check the polygon coordinates
You can extract the polygon exterior coordinates back into a pandas DataFrame:
1 2 3 4 5 6 | coords_df = pd.DataFrame( list(gdf_topaz_polygon.geometry.iloc[0].exterior.coords), columns=["longitude", "latitude"] ) coords_df |
| longitude | latitude |
|---|---|
| -120.028905 | 35.36099 |
| -120.027158 | 35.36099 |
| -120.02721 | 35.3533 |
| -120.032477 | 35.353315 |
| -120.032578 | 35.359217 |
| -120.037711 | 35.359246 |
| -120.037687 | 35.368248 |
| -120.039398 | 35.368279 |
| -120.039423 | 35.378957 |
| -120.037763 | 35.378977 |
| -120.037782 | 35.380434 |
| -120.02723 | 35.380497 |
| -120.027087 | 35.363782 |
| -120.028902 | 35.363782 |
| -120.028905 | 35.36099 |
This is useful if you want to inspect or export the polygon coordinates.
Compute the polygon area in square kilometers
Do not calculate area directly in latitude/longitude coordinates. In EPSG:4326, the geometry units are degrees, not meters.
Instead, estimate a local projected CRS and calculate the area there.
1 2 3 | utm_crs = gdf_topaz_polygon.estimate_utm_crs() print("Estimated UTM CRS:", utm_crs) |
Project the polygon to the estimated UTM CRS:
1 | gdf_topaz_polygon_m = gdf_topaz_polygon.to_crs(utm_crs) |
Now calculate the area in square kilometers:
1 2 3 4 5 | gdf_topaz_polygon["area_km2"] = ( gdf_topaz_polygon_m.geometry.area.values / 1_000_000 ) gdf_topaz_polygon[["name", "area_km2", "geometry"]] |
Estimated UTM CRS: EPSG:32610
| name | area_km2 | geometry |
|---|---|---|
| Topaz Solar Farm - extracted largest polygon | 2.7147696427931787 | POLYGON ((-120.028905 35.36099, -120.027158 35.36099, -120.02721 35.3533, -120.032477 35.353315, -120.032578 35.359217, -120.037711 35.359246, -120.037687 35.368248, -120.039398 35.368279, -120.039423 35.378957, -120.037763 35.378977, -120.037782 35.380434, -120.02723 35.380497, -120.027087 35.363782, -120.028902 35.363782, -120.028905 35.36099)) |
Create manual test points
Before generating many random points, it is useful to create a few manual points.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | df_test_points = pd.DataFrame({ "point_id": [1, 2, 3, 4, 5, 6], "description": [ "likely inside polygon", "inside or close to polygon", "east of polygon", "west of polygon", "north of polygon", "south of polygon", ], "longitude": [ -120.0320, -120.0285, -120.0245, -120.0420, -120.0330, -120.0330, ], "latitude": [ 35.3650, 35.3700, 35.3660, 35.3680, 35.3830, 35.3510, ], }) |
Convert the pandas DataFrame to a GeoDataFrame:
1 2 3 4 5 6 7 8 9 10 | gdf_test_points = gpd.GeoDataFrame( df_test_points, geometry=gpd.points_from_xy( df_test_points["longitude"], df_test_points["latitude"], ), crs="EPSG:4326", ) gdf_test_points |
| point_id | description | longitude | latitude | geometry | intersects_polygon | intersects_buffer | intersection_class |
|---|---|---|---|---|---|---|---|
| 1 | likely inside polygon | -120.032 | 35.365 | POINT (-120.032 35.365) | True | True | Inside polygon |
| 2 | inside or close to polygon | -120.0285 | 35.37 | POINT (-120.0285 35.37) | True | True | Inside polygon |
| 3 | east of polygon | -120.0245 | 35.366 | POINT (-120.0245 35.366) | False | True | Inside buffer only |
| 4 | west of polygon | -120.042 | 35.368 | POINT (-120.042 35.368) | False | True | Inside buffer only |
| 5 | north of polygon | -120.033 | 35.383 | POINT (-120.033 35.383) | False | True | Inside buffer only |
| 6 | south of polygon | -120.033 | 35.351 | POINT (-120.033 35.351) | False | True | Inside buffer only |
Test whether points intersect the polygon
Extract the polygon geometry:
1 | topaz_geom = gdf_topaz_polygon.geometry.iloc[0] |
Then use intersects():
1 2 3 | gdf_test_points["intersects_polygon"] = ( gdf_test_points.geometry.intersects(topaz_geom) ) |
Display the result:
1 2 3 4 5 6 7 8 9 | gdf_test_points[ [ "point_id", "description", "longitude", "latitude", "intersects_polygon", ] ] |
| point_id | description | longitude | latitude | intersects_polygon |
|---|---|---|---|---|
| 1 | likely inside polygon | -120.032 | 35.365 | True |
| 2 | inside or close to polygon | -120.0285 | 35.37 | True |
| 3 | east of polygon | -120.0245 | 35.366 | False |
| 4 | west of polygon | -120.042 | 35.368 | False |
| 5 | north of polygon | -120.033 | 35.383 | False |
| 6 | south of polygon | -120.033 | 35.351 | False |
The new column intersects_polygon contains True or False.
Why use intersects?
For points and polygons, you can use several spatial predicates:
1 2 3 | gdf_test_points.geometry.within(topaz_geom) gdf_test_points.geometry.intersects(topaz_geom) gdf_test_points.geometry.touches(topaz_geom) |
The difference matters:
withinis true only when the point is strictly inside the polygon.touchesis true when the point is exactly on the polygon boundary.intersectsis true if the point is inside or touches the polygon.
For many Earth observation applications, intersects is a good choice because it includes boundary cases.
Create a buffer around the polygon
A buffer is useful when the polygon boundary is approximate, or when the point location has some uncertainty.
For example, a satellite fire pixel, a persistent thermal anomaly, or a geolocation point may be slightly shifted. A buffer allows us to include points close to the polygon.
Important: create the buffer in a projected CRS using meters.
1 | buffer_m = 1000 |
Project the polygon and points:
1 2 | gdf_topaz_polygon_m = gdf_topaz_polygon.to_crs(utm_crs) gdf_test_points_m = gdf_test_points.to_crs(utm_crs) |
Create the buffer:
1 | topaz_buffer_m = gdf_topaz_polygon_m.geometry.iloc[0].buffer(buffer_m) |
Create a GeoDataFrame for the buffer:
1 2 3 4 5 | gdf_topaz_buffer_m = gpd.GeoDataFrame( {"name": [f"Topaz Solar Farm {buffer_m} m buffer"]}, geometry=[topaz_buffer_m], crs=utm_crs, ) |
Convert the buffer back to latitude/longitude for mapping:
1 | gdf_topaz_buffer = gdf_topaz_buffer_m.to_crs("EPSG:4326") |
Now test whether each point intersects the buffer:
1 2 3 4 5 6 7 | gdf_test_points_m["intersects_buffer"] = ( gdf_test_points_m.geometry.intersects(topaz_buffer_m) ) gdf_test_points["intersects_buffer"] = ( gdf_test_points_m["intersects_buffer"].values ) |
Display the result:
1 2 3 4 5 6 7 8 9 10 | gdf_test_points[ [ "point_id", "description", "longitude", "latitude", "intersects_polygon", "intersects_buffer", ] ] |
| point_id | description | longitude | latitude | intersects_polygon | intersects_buffer |
|---|---|---|---|---|---|
| 1 | likely inside polygon | -120.032 | 35.365 | True | True |
| 2 | inside or close to polygon | -120.0285 | 35.37 | True | True |
| 3 | east of polygon | -120.0245 | 35.366 | False | True |
| 4 | west of polygon | -120.042 | 35.368 | False | True |
| 5 | north of polygon | -120.033 | 35.383 | False | True |
| 6 | south of polygon | -120.033 | 35.351 | False | True |
Add a readable class column
The Boolean columns are useful, but a readable class is better for mapping.
1 2 3 4 5 6 7 | def classify_intersection(row): if row["intersects_polygon"]: return "Inside polygon" elif row["intersects_buffer"]: return "Inside buffer only" else: return "Outside" |
Apply the function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | gdf_test_points["intersection_class"] = gdf_test_points.apply( classify_intersection, axis=1, ) gdf_test_points[ [ "point_id", "description", "intersects_polygon", "intersects_buffer", "intersection_class", ] ] |
| point_id | description | intersects_polygon | intersects_buffer | intersection_class |
|---|---|---|---|---|
| 1 | likely inside polygon | True | True | Inside polygon |
| 2 | inside or close to polygon | True | True | Inside polygon |
| 3 | east of polygon | False | True | Inside buffer only |
| 4 | west of polygon | False | True | Inside buffer only |
| 5 | north of polygon | False | True | Inside buffer only |
| 6 | south of polygon | False | True | Inside buffer only |
Generate random points around the polygon
Now let’s generate random points around the polygon.
First, get the polygon bounds:
1 | min_lon, min_lat, max_lon, max_lat = gdf_topaz_polygon.total_bounds |
Add some padding around the polygon:
1 2 | padding_lon = 0.015 padding_lat = 0.015 |
Generate random points:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | rng = np.random.default_rng(42) n_points = 100 random_lons = rng.uniform( min_lon - padding_lon, max_lon + padding_lon, n_points, ) random_lats = rng.uniform( min_lat - padding_lat, max_lat + padding_lat, n_points, ) |
Create a DataFrame:
1 2 3 4 5 | df_random_points = pd.DataFrame({ "point_id": range(1, n_points + 1), "longitude": random_lons, "latitude": random_lats, }) |
Convert to a GeoDataFrame:
1 2 3 4 5 6 7 8 9 10 | gdf_random_points = gpd.GeoDataFrame( df_random_points, geometry=gpd.points_from_xy( df_random_points["longitude"], df_random_points["latitude"], ), crs="EPSG:4326", ) gdf_random_points.head() |
| point_id | longitude | latitude | geometry | intersects_polygon | intersects_buffer | intersection_class |
|---|---|---|---|---|---|---|
| 1 | -120.02165679672834 | 35.3902680897664 | POINT (-120.02165679672834 35.3902680897664) | False | False | Outside |
| 2 | -120.03584264237466 | 35.37832114893257 | POINT (-120.03584264237466 35.37832114893257) | True | True | Inside polygon |
| 3 | -120.01807339846263 | 35.3535069641856 | POINT (-120.01807339846263 35.3535069641856) | False | True | Inside buffer only |
| 4 | -120.02489922712174 | 35.39373398125516 | POINT (-120.02489922712174 35.39373398125516) | False | False | Outside |
| 5 | -120.05043590779982 | 35.38284221545413 | POINT (-120.05043590779982 35.38284221545413) | False | False | Outside |
Test random points against the polygon and buffer
Test the original polygon:
1 2 3 | gdf_random_points["intersects_polygon"] = ( gdf_random_points.geometry.intersects(topaz_geom) ) |
Test the buffer:
1 2 3 4 5 6 7 8 9 | gdf_random_points_m = gdf_random_points.to_crs(utm_crs) gdf_random_points_m["intersects_buffer"] = ( gdf_random_points_m.geometry.intersects(topaz_buffer_m) ) gdf_random_points["intersects_buffer"] = ( gdf_random_points_m["intersects_buffer"].values ) |
Add the readable class:
1 2 3 4 5 6 | gdf_random_points["intersection_class"] = gdf_random_points.apply( classify_intersection, axis=1, ) gdf_random_points.head(15) |
| point_id | longitude | latitude | geometry | intersects_polygon | intersects_buffer | intersection_class |
|---|---|---|---|---|---|---|
| 1 | -120.02165679672834 | 35.3902680897664 | POINT (-120.02165679672834 35.3902680897664) | False | False | Outside |
| 2 | -120.03584264237466 | 35.37832114893257 | POINT (-120.03584264237466 35.37832114893257) | True | True | Inside polygon |
| 3 | -120.01807339846263 | 35.3535069641856 | POINT (-120.01807339846263 35.3535069641856) | False | True | Inside buffer only |
| 4 | -120.02489922712174 | 35.39373398125516 | POINT (-120.02489922712174 35.39373398125516) | False | False | Outside |
| 5 | -120.05043590779982 | 35.38284221545413 | POINT (-120.05043590779982 35.38284221545413) | False | False | Outside |
| 6 | -120.0131190521211 | 35.37930396814932 | POINT (-120.0131190521211 35.37930396814932) | False | False | Outside |
| 7 | -120.02219938957653 | 35.36400212983811 | POINT (-120.02219938957653 35.36400212983811) | False | True | Inside buffer only |
| 8 | -120.02114418157178 | 35.35387140061285 | POINT (-120.02114418157178 35.35387140061285) | False | True | Inside buffer only |
| 9 | -120.04899918124704 | 35.34381327386229 | POINT (-120.04899918124704 35.34381327386229) | False | False | Outside |
| 10 | -120.03535546093325 | 35.38992614927512 | POINT (-120.03535546093325 35.38992614927512) | False | False | Outside |
| 11 | -120.03872489484608 | 35.36436903644961 | POINT (-120.03872489484608 35.36436903644961) | False | True | Inside buffer only |
| 12 | -120.0151874774321 | 35.34987457737619 | POINT (-120.0151874774321 35.34987457737619) | False | False | Outside |
| 13 | -120.02716432627626 | 35.355799801031544 | POINT (-120.02716432627626 35.355799801031544) | False | True | Inside buffer only |
| 14 | -120.01959056434056 | 35.37142962168477 | POINT (-120.01959056434056 35.37142962168477) | False | True | Inside buffer only |
| 15 | -120.03565061647845 | 35.34841087286577 | POINT (-120.03565061647845 35.34841087286577) | False | True | Inside buffer only |
Count points by class
1 2 3 4 5 6 7 8 9 | summary = ( gdf_random_points .groupby("intersection_class") .size() .reset_index(name="n_points") .sort_values("intersection_class") ) summary |
| intersection_class | n_points |
|---|---|
| Inside buffer only | 53 |
| Inside polygon | 11 |
| Outside | 36 |
This gives a quick summary of how many points are:
- inside the polygon;
- inside the buffer only;
- outside both.
Spatial join alternative
For a single polygon, this is enough:
1 | gdf_random_points.geometry.intersects(topaz_geom) |
But if you have many polygons, gpd.sjoin() is often more convenient.
1 2 3 4 5 6 | matches = gpd.sjoin( gdf_random_points, gdf_topaz_polygon[["name", "geometry"]], how="left", predicate="intersects", ) |
Inspect the result:
1 2 3 4 5 6 7 8 9 | matches[ [ "point_id", "longitude", "latitude", "intersects_polygon", "name", ] ].head(20) |
| point_id | longitude | latitude | intersects_polygon | name |
|---|---|---|---|---|
| 1 | -120.02165679672834 | 35.3902680897664 | False | nan |
| 2 | -120.03584264237466 | 35.37832114893257 | True | Topaz Solar Farm - extracted largest polygon |
| 3 | -120.01807339846263 | 35.3535069641856 | False | nan |
| 4 | -120.02489922712174 | 35.39373398125516 | False | nan |
| 5 | -120.05043590779982 | 35.38284221545413 | False | nan |
| 6 | -120.0131190521211 | 35.37930396814932 | False | nan |
| 7 | -120.02219938957653 | 35.36400212983811 | False | nan |
| 8 | -120.02114418157178 | 35.35387140061285 | False | nan |
| 9 | -120.04899918124704 | 35.34381327386229 | False | nan |
| 10 | -120.03535546093325 | 35.38992614927512 | False | nan |
| 11 | -120.03872489484608 | 35.36436903644961 | False | nan |
| 12 | -120.0151874774321 | 35.34987457737619 | False | nan |
| 13 | -120.02716432627626 | 35.355799801031544 | False | nan |
| 14 | -120.01959056434056 | 35.37142962168477 | False | nan |
| 15 | -120.03565061647845 | 35.34841087286577 | False | nan |
| 16 | -120.04480262147452 | 35.38729576720723 | False | nan |
| 17 | -120.0309440984569 | 35.38168504154798 | False | nan |
| 18 | -120.05172123264558 | 35.37945112269152 | False | nan |
| 19 | -120.01938440670251 | 35.36301442559601 | False | nan |
| 20 | -120.02768085599877 | 35.37418018376166 | True | Topaz Solar Farm - extracted largest polygon |
Points intersecting the polygon will receive the polygon name.
Points outside the polygon will have missing values in the joined polygon columns.
Export polygon, buffer, and points to GeoJSON
GeoJSON is useful because it can be displayed directly in Leaflet.
1 2 | output_dir = Path("topaz_polygon_buffer_leaflet_example") output_dir.mkdir(exist_ok=True) |
Save the polygon:
1 2 3 4 | gdf_topaz_polygon.to_file( output_dir / "topaz_solar_farm_polygon.geojson", driver="GeoJSON", ) |
Save the buffer:
1 2 3 4 | gdf_topaz_buffer.to_file( output_dir / "topaz_solar_farm_buffer.geojson", driver="GeoJSON", ) |
Save the random points:
1 2 3 4 | gdf_random_points.to_file( output_dir / "random_points_topaz.geojson", driver="GeoJSON", ) |
Save the manual test points:
1 2 3 4 | gdf_test_points.to_file( output_dir / "manual_test_points_topaz.geojson", driver="GeoJSON", ) |
Check the files:
1 2 3 | print("Created files:") for path in sorted(output_dir.glob("*.geojson")): print(path) |
Create a Leaflet map with Folium
Folium allows us to create a Leaflet map directly from Python.
centroid = gdf_topaz_polygon.geometry.iloc[0].centroid
1 2 3 4 5 | m = folium.Map( location=[centroid.y, centroid.x], zoom_start=14, tiles="OpenStreetMap", ) |
Add the buffer first:
1 2 3 4 5 6 7 8 9 10 | folium.GeoJson( gdf_topaz_buffer, name=f"{buffer_m} m buffer", style_function=lambda feature: { "color": "#9333ea", "weight": 2, "fillColor": "#9333ea", "fillOpacity": 0.15, }, ).add_to(m) |
Add the original polygon:
1 2 3 4 5 6 7 8 9 10 11 | folium.GeoJson( gdf_topaz_polygon, name="Topaz polygon", style_function=lambda feature: { "color": "#2563eb", "weight": 3, "fillColor": "#2563eb", "fillOpacity": 0.25, }, tooltip=folium.GeoJsonTooltip(fields=["name", "area_km2"]), ).add_to(m) |
Define colors for the points:
1 2 3 4 5 | color_lookup = { "Inside polygon": "green", "Inside buffer only": "orange", "Outside": "red", } |
Add the random points:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | for _, row in gdf_random_points.iterrows(): color = color_lookup[row["intersection_class"]] popup_html = ( f"<b>Random point ID:</b> {row['point_id']}<br>" f"<b>Intersects polygon:</b> {row['intersects_polygon']}<br>" f"<b>Intersects buffer:</b> {row['intersects_buffer']}<br>" f"<b>Class:</b> {row['intersection_class']}" ) folium.CircleMarker( location=[row.geometry.y, row.geometry.x], radius=4, color=color, fill=True, fill_color=color, fill_opacity=0.85, popup=folium.Popup(popup_html, max_width=260), ).add_to(m) |
Add manual test points with larger markers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | for _, row in gdf_test_points.iterrows(): color = color_lookup[row["intersection_class"]] popup_html = ( f"<b>Manual point ID:</b> {row['point_id']}<br>" f"<b>Description:</b> {row['description']}<br>" f"<b>Intersects polygon:</b> {row['intersects_polygon']}<br>" f"<b>Intersects buffer:</b> {row['intersects_buffer']}<br>" f"<b>Class:</b> {row['intersection_class']}" ) folium.CircleMarker( location=[row.geometry.y, row.geometry.x], radius=8, color="black", weight=2, fill=True, fill_color=color, fill_opacity=0.95, popup=folium.Popup(popup_html, max_width=280), ).add_to(m) |
Add a layer control:
1 2 3 | folium.LayerControl().add_to(m) m |

The map colors are:
- green: point intersects the polygon;
- orange: point intersects only the buffer;
- red: point is outside both.
Save the map as HTML
1 2 3 4 5 | html_map_path = output_dir / "topaz_polygon_buffer_intersection_map.html" m.save(html_map_path) print("Saved map to:", html_map_path) |
You can open the saved HTML file in a browser.
Create a standalone Leaflet page
If you want to create a pure Leaflet page, you can use the exported GeoJSON files.
Create a file called:
1 | index.html |
inside the folder:
1 | topaz_polygon_buffer_leaflet_example
|
Then use this HTML template:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Topaz Solar Farm Polygon Buffer Example</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <style> body { margin: 0; font-family: Arial, sans-serif; background: #f4f4f4; } header { padding: 16px 24px; background: #111827; color: white; } header h1 { margin: 0; font-size: 22px; } header p { margin: 6px 0 0 0; color: #d1d5db; font-size: 14px; } #map { width: 100%; height: 720px; } .legend { position: absolute; right: 16px; bottom: 24px; z-index: 1000; background: white; padding: 12px 14px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.25); font-size: 13px; line-height: 1.5; } .legend-item { display: flex; align-items: center; gap: 8px; } .dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; } .dot-inside { background: #16a34a; } .dot-buffer { background: #f59e0b; } .dot-outside { background: #dc2626; } .line-polygon { width: 18px; height: 4px; background: #2563eb; display: inline-block; } .line-buffer { width: 18px; height: 4px; background: #9333ea; display: inline-block; opacity: 0.5; } </style> </head> <body> <header> <h1>Testing Point Intersections with a Topaz Solar Farm Polygon</h1> <p> Green points intersect the polygon. Orange points intersect only the buffer. Red points are outside both. </p> </header> <div id="map"></div> <div class="legend"> <div class="legend-item"> <span class="dot dot-inside"></span> Point intersects polygon </div> <div class="legend-item"> <span class="dot dot-buffer"></span> Point intersects buffer only </div> <div class="legend-item"> <span class="dot dot-outside"></span> Point outside polygon and buffer </div> <div class="legend-item"> <span class="line-polygon"></span> Topaz polygon </div> <div class="legend-item"> <span class="line-buffer"></span> Buffer zone </div> </div> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script> const map = L.map("map").setView([35.3669, -120.0333], 14); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap contributors" }).addTo(map); function pointColor(props) { if (props.intersects_polygon === true || props.intersects_polygon === "True") { return "#16a34a"; } if (props.intersects_buffer === true || props.intersects_buffer === "True") { return "#f59e0b"; } return "#dc2626"; } fetch("topaz_solar_farm_buffer.geojson") .then(response => response.json()) .then(data => { L.geoJSON(data, { style: { color: "#9333ea", weight: 2, fillColor: "#9333ea", fillOpacity: 0.15 } }).addTo(map); }); fetch("topaz_solar_farm_polygon.geojson") .then(response => response.json()) .then(data => { const polygonLayer = L.geoJSON(data, { style: { color: "#2563eb", weight: 3, fillColor: "#2563eb", fillOpacity: 0.25 }, onEachFeature: function(feature, layer) { layer.bindPopup(feature.properties.name); } }).addTo(map); map.fitBounds(polygonLayer.getBounds(), { padding: [30, 30] }); }); fetch("random_points_topaz.geojson") .then(response => response.json()) .then(data => { L.geoJSON(data, { pointToLayer: function(feature, latlng) { const color = pointColor(feature.properties); return L.circleMarker(latlng, { radius: 5, color: color, fillColor: color, fillOpacity: 0.85, weight: 1 }); }, onEachFeature: function(feature, layer) { const props = feature.properties; const popupContent = ` <strong>Point ID:</strong> ${props.point_id}<br> <strong>Intersects polygon:</strong> ${props.intersects_polygon}<br> <strong>Intersects buffer:</strong> ${props.intersects_buffer}<br> <strong>Class:</strong> ${props.intersection_class} `; layer.bindPopup(popupContent); } }).addTo(map); }); </script> </body> </html> |
Run the Leaflet example locally
Because the HTML file uses fetch() to load local GeoJSON files, it is better to use a local web server.
From the terminal:
1 2 | cd topaz_polygon_buffer_leaflet_example python -m http.server 8000 |
Then open:
1 | http://localhost:8000/index.html |
Common mistake: creating buffers in latitude/longitude
This is a common mistake:
1 | gdf_topaz_polygon.geometry.buffer(1000) |
If the CRS is EPSG:4326, the buffer distance is interpreted in degrees, not meters.
The safer workflow is:
1 2 3 4 5 | utm_crs = gdf_topaz_polygon.estimate_utm_crs() gdf_topaz_polygon_m = gdf_topaz_polygon.to_crs(utm_crs) buffer_polygon_m = gdf_topaz_polygon_m.geometry.iloc[0].buffer(1000) |
Then convert the buffer back to longitude/latitude for Leaflet:
1 2 3 4 5 | gdf_topaz_buffer = gpd.GeoDataFrame( {"name": ["1000 m buffer"]}, geometry=[buffer_polygon_m], crs=utm_crs, ).to_crs("EPSG:4326") |
Conclusion
GeoPandas makes it easy to test whether points intersect a polygon or a buffer zone.
The main workflow is:
- Create or load a polygon.
- Convert point coordinates to a GeoDataFrame.
- Use
geometry.intersects(polygon)to create a Boolean column. - Project to a meter-based CRS before creating buffers.
- Use the buffered polygon to create a second Boolean column.
- Export the result to GeoJSON.
- Display the result with Folium or Leaflet.
This approach is useful for many Earth observation workflows, including persistent thermal anomaly analysis, fire detection validation, solar farm mapping, and spatial filtering of satellite observations.
References
| Links | Site |
|---|---|
| GeoPandas documentation | GeoPandas |
| GeoPandas spatial joins | GeoPandas |
| GeoPandas estimate_utm_crs | GeoPandas |
| Shapely Polygon documentation | Shapely |
| Shapely intersects documentation | Shapely |
| Folium documentation | Folium |
| Leaflet GeoJSON tutorial | Leaflet |
| OpenStreetMap | OpenStreetMap |