Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/pyshp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# pyshp Examples

Each sub-directory contains a self-contained example. The order in
which the examples are to appear is specified in `order.json` (an
array of directory names in the expected order).

In each example directory you'll find:

* `config.toml` - must conform to the specification outlined here:
https://docs.pyscript.net/latest/user-guide/configuration/ This is
parsed and ultimately turned into a JSON representation as part of
the package's API object.
* `setup.py` - Python code for contextual and environmental setup,
NOT SEEN BY THE END USER, but is run before the `code.py` code is
evaluated. Allows us to create useful (IPython) shims, avoid
repeating boilerplate and whatnot.
* `code.py` - the actual code added to the editor which forms the
practical example of using the package.
123 changes: 123 additions & 0 deletions examples/pyshp/filter_and_edit/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# ---------------------------------------------------------------------
# A realistic editing workflow: build a source shapefile, then stream
# through it filtering by attribute and bounding box, writing only the
# matching features (and only the fields we care about) to a new file.
# ---------------------------------------------------------------------
import shapefile
import io
import matplotlib.pyplot as plt


heading("Source data: river segments across two basins")
note(
"We'll synthesize a polyline shapefile of fictional river "
"segments tagged by basin and length, then produce a derived "
"shapefile containing only the long segments in one basin."
)

# Build the source shapefile in memory.
src_shp, src_shx, src_dbf = io.BytesIO(), io.BytesIO(), io.BytesIO()

rivers = [
("Otter Brook", "North", 4.2, [(0, 0), (1, 2), (2, 5)]),
("Heron Creek", "North", 1.1, [(2, 5), (3, 5)]),
("Pine River", "North", 8.7, [(3, 5), (5, 6), (8, 8), (10, 9)]),
("Willow Run", "South", 2.5, [(0, -1), (2, -2), (4, -2)]),
("Birch Stream", "South", 6.0, [(4, -2), (7, -3), (10, -4)]),
("Cedar Wash", "South", 0.9, [(7, -3), (8, -3)]),
]

with shapefile.Writer(shp=src_shp, shx=src_shx, dbf=src_dbf) as writer:
writer.field("name", "C", size=40)
writer.field("basin", "C", size=10)
writer.field("length_km", "N", decimal=1)
writer.field("notes", "C", size=80) # a field we'll later drop

for name, basin, length, coords in rivers:
writer.line([coords])
writer.record(name, basin, length, "auto-generated sample")

heading("Streaming filter and rewrite", level=3)
note(
"We open the source with a Reader, request only the fields we "
"need via <code>iterShapeRecords(fields=...)</code>, and use a "
"<code>bbox</code> argument so the Reader's spatial index "
"skips features whose bounding boxes can't possibly match."
)

for buf in (src_shp, src_shx, src_dbf):
buf.seek(0)

dst_shp, dst_shx, dst_dbf = io.BytesIO(), io.BytesIO(), io.BytesIO()

# Region of interest: a bounding box covering the eastern half.
region_bbox = [3, -5, 11, 10]
keep_fields = ["name", "basin", "length_km"]

reader = shapefile.Reader(shp=src_shp, shx=src_shx, dbf=src_dbf)
writer = shapefile.Writer(shp=dst_shp, shx=dst_shx, dbf=dst_dbf)

# Copy only the fields we want to keep. PyShp returns each field as a
# plain list, [name, type, size, decimal], not an object with named
# attributes -- so we index by position. The first entry is always a
# DeletionFlag, which we skip.
for field in reader.fields[1:]:
if field[0] in keep_fields:
writer.field(*field)

kept = []
for shape_record in reader.iterShapeRecords(
bbox=region_bbox, fields=keep_fields,
):
record = shape_record.record
if record["basin"] == "North" and record["length_km"] >= 4.0:
writer.shape(shape_record.shape)
writer.record(*[record[name] for name in keep_fields])
kept.append(record["name"])

writer.close()
reader.close()

note(f"Kept {len(kept)} feature(s): " + ", ".join(kept))

# Read the new shapefile back and visualize the result.
for buf in (dst_shp, dst_shx, dst_dbf):
buf.seek(0)

result = shapefile.Reader(shp=dst_shp, shx=dst_shx, dbf=dst_dbf)

note(
f"Output shapefile: <code>{result.shapeTypeName}</code>, "
f"{len(result)} feature(s), "
f"{len(result.fields) - 1} field(s) "
"(notice the <code>notes</code> field was dropped)."
)

fig, ax = plt.subplots(figsize=(7, 4))

# Draw the original rivers in light gray for context.
src_shp.seek(0); src_shx.seek(0); src_dbf.seek(0)
context = shapefile.Reader(shp=src_shp, shx=src_shx, dbf=src_dbf)
for shape in context.iterShapes():
xs, ys = zip(*shape.points)
ax.plot(xs, ys, color="lightgray", linewidth=1)
context.close()

# Overlay the surviving features in bold.
for shape_record in result.iterShapeRecords():
xs, ys = zip(*shape_record.shape.points)
ax.plot(xs, ys, linewidth=2.5,
label=shape_record.record["name"])

# Show the query bounding box.
x0, y0, x1, y1 = region_bbox
ax.plot([x0, x1, x1, x0, x0], [y0, y0, y1, y1, y0],
linestyle="--", color="darkorange", label="query bbox")

ax.set_aspect("equal")
ax.set_title("Long rivers in the North basin, within the query box")
ax.legend(loc="lower right", fontsize=8)
fig.tight_layout()
display(fig, append=True)

result.close()
1 change: 1 addition & 0 deletions examples/pyshp/filter_and_edit/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["pyshp", "matplotlib"]
18 changes: 18 additions & 0 deletions examples/pyshp/filter_and_edit/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Lightweight setup for the third example."""
import js
from pyscript import window, HTML, display as _display

js.alert = window.alert


def display(*args, **kwargs):
return _display(*args, **kwargs, target=__pyscript_display_target__)


def heading(text, level=2):
display(HTML(f"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)

5 changes: 5 additions & 0 deletions examples/pyshp/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"write_and_read_points",
"polygons_and_geojson",
"filter_and_edit"
]
120 changes: 120 additions & 0 deletions examples/pyshp/polygons_and_geojson/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# ---------------------------------------------------------------------
# Polygon shapefiles, holes, and GeoJSON via __geo_interface__.
# ---------------------------------------------------------------------

import shapefile
import io
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.patches import PathPatch

heading("Tiny parks dataset")
note(
"Polygons in shapefiles must be closed (the last point repeats "
"the first). Crucially, the shapefile format has no flag to "
"distinguish an outer ring from a hole — the only signal is "
"winding order: outer rings clockwise, holes counterclockwise. "
"Get this wrong and tools that consume the file (including "
"PyShp's own GeoJSON conversion) will misread your geometry."
)

shp_buf, shx_buf, dbf_buf = io.BytesIO(), io.BytesIO(), io.BytesIO()

# A square park with a square pond cut out, plus a triangular plaza.
# Outer rings are clockwise; holes are counterclockwise.
park_outer = [(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)] # clockwise
pond_hole = [(3, 3), (3, 7), (7, 7), (7, 3), (3, 3)] # counterclockwise
plaza = [(15, 2), (17, 8), (20, 2), (15, 2)] # clockwise

with shapefile.Writer(shp=shp_buf, shx=shx_buf, dbf=dbf_buf) as writer:
writer.field("name", "C", size=30)
writer.field("area_sqm", "N", decimal=1)

# A single polygon feature with an outer ring and a hole.
writer.poly([park_outer, pond_hole])
writer.record("Riverside Park", 84.0)

# A separate polygon feature with no holes.
writer.poly([plaza])
writer.record("Civic Plaza", 15.0)

for buf in (shp_buf, shx_buf, dbf_buf):
buf.seek(0)

reader = shapefile.Reader(shp=shp_buf, shx=shx_buf, dbf=dbf_buf)

heading("Inspecting parts and points", level=3)
for shape_record in reader.iterShapeRecords():
shape = shape_record.shape
name = shape_record.record["name"]
note(
f"<strong>{name}</strong>: shape type "
f"<code>{shape.shapeTypeName}</code>, "
f"{len(shape.points)} points across "
f"{len(shape.parts)} ring(s) "
f"(part start indices: {list(shape.parts)})."
)

heading("GeoJSON for free", level=3)
note(
"Every Shape, Record, and Reader implements "
"<code>__geo_interface__</code>, so converting to GeoJSON is "
"a one-liner that any geospatial tool can consume."
)
geojson = reader.__geo_interface__
display(HTML(
f"<pre>type: {geojson['type']}\\n"
f"features: {len(geojson['features'])}\\n"
f"first geometry type: {geojson['features'][0]['geometry']['type']}"
"</pre>"
), append=True)

heading("Plotting polygons with real holes", level=3)
note(
"GeoJSON's nested-ring structure encodes holes semantically, but "
"matplotlib needs you to translate that into a compound "
"<code>Path</code>: outer ring plus inner rings as sub-paths in a "
"single <code>PathPatch</code>. This renders holes as genuinely "
"transparent — unlike the common shortcut of overpainting with a "
"white fill, which breaks the moment you have a non-white "
"background or layered features."
)

# Plot the polygons using their GeoJSON coordinates.
fig, ax = plt.subplots(figsize=(7, 4))
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]

for i, feature in enumerate(geojson["features"]):
geom = feature["geometry"]
name = feature["properties"]["name"]
color = colors[i % len(colors)]
# GeoJSON nests rings as [[outer, hole, ...]] for Polygons,
# and one level deeper for MultiPolygons.
polygons = (
geom["coordinates"]
if geom["type"] == "MultiPolygon"
else [geom["coordinates"]]
)
for rings in polygons:
# Build a compound path: outer ring + holes as sub-paths.
# Matplotlib renders the holes as genuinely transparent when
# the path alternates winding between outer and inner rings.
vertices = []
codes = []
for ring in rings:
vertices.extend(ring)
codes.append(Path.MOVETO)
codes.extend([Path.LINETO] * (len(ring) - 2))
codes.append(Path.CLOSEPOLY)
path = Path(vertices, codes)
patch = PathPatch(path, facecolor=color, alpha=0.4, label=name)
ax.add_patch(patch)

ax.set_aspect("equal")
ax.autoscale_view()
ax.set_title("Parks polygons (with a pond-shaped hole)")
ax.legend(loc="upper right")
fig.tight_layout()
display(fig, append=True)

reader.close()
1 change: 1 addition & 0 deletions examples/pyshp/polygons_and_geojson/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["pyshp", "matplotlib"]
17 changes: 17 additions & 0 deletions examples/pyshp/polygons_and_geojson/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Lightweight setup for the second example."""
import js
from pyscript import window, HTML, display as _display

js.alert = window.alert


def display(*args, **kwargs):
return _display(*args, **kwargs, target=__pyscript_display_target__)


def heading(text, level=2):
display(HTML(f"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
82 changes: 82 additions & 0 deletions examples/pyshp/write_and_read_points/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""
A first look at PyShp: write a small Point shapefile entirely in
memory, then read it back to verify the geometry and attributes.

PyShp is the pure-Python reader/writer for the ESRI Shapefile format.
The PyPI distribution is named `pyshp`, but the import name is
`shapefile`. Docs and source: https://github.com/GeospatialPython/pyshp
"""
from IPython.core.display import display, HTML
# pyshp is imported as the `shapefile` module.
import shapefile
import io


heading("A handful of lighthouses")
note(
"A shapefile is really three files working together: "
"<code>.shp</code> (geometry), <code>.shx</code> (index), "
"and <code>.dbf</code> (attribute table). PyShp can read and "
"write each one to a file-like object, which is exactly what "
"we want in the browser."
)

# Three buffers stand in for the three files on disk.
shp_buffer = io.BytesIO()
shx_buffer = io.BytesIO()
dbf_buffer = io.BytesIO()

# A small dataset: famous lighthouses with their (lon, lat) coordinates.
lighthouses = [
("Eddystone", -4.2636, 50.1922, 1882),
("Fastnet", -9.6033, 51.3833, 1904),
("Cape Hatteras", -75.5290, 35.2503, 1870),
("Tower of Hercules", -8.4063, 43.3863, 100),
]

# Build the shapefile by streaming records into the Writer.
with shapefile.Writer(
shp=shp_buffer, shx=shx_buffer, dbf=dbf_buffer,
) as writer:
# Define the attribute schema before adding any records.
writer.field("name", "C", size=40) # text
writer.field("year", "N", decimal=0) # integer year built

for name, lon, lat, year in lighthouses:
writer.point(lon, lat)
writer.record(name, year)

note(f"Wrote {len(lighthouses)} point features to in-memory buffers.")

# Reading is symmetric: hand the buffers to a Reader.
for buf in (shp_buffer, shx_buffer, dbf_buffer):
buf.seek(0)

reader = shapefile.Reader(
shp=shp_buffer, shx=shx_buffer, dbf=dbf_buffer,
)

heading("What did we just write?", level=3)
note(
f"Shape type: <code>{reader.shapeTypeName}</code>. "
f"Feature count: <strong>{len(reader)}</strong>. "
f"Bounding box: <code>{tuple(round(c, 3) for c in reader.bbox)}</code>."
)

heading("Iterating shape/record pairs", level=3)
rows = []
for shape_record in reader.iterShapeRecords():
(lon, lat) = shape_record.shape.points[0]
name = shape_record.record["name"]
year = shape_record.record["year"]
rows.append(f"<tr><td>{name}</td><td>{year}</td>"
f"<td>{lon:.3f}, {lat:.3f}</td></tr>")

table = (
"<table><thead><tr><th>Name</th><th>Built</th>"
"<th>Lon, Lat</th></tr></thead><tbody>"
+ "".join(rows) + "</tbody></table>"
)
display(HTML(table), append=True)

reader.close()
1 change: 1 addition & 0 deletions examples/pyshp/write_and_read_points/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["pyshp"]
Loading