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/affine/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# affine 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.
5 changes: 5 additions & 0 deletions examples/affine/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"transform_basics",
"transforming_a_shape",
"raster_pixel_to_world"
]
75 changes: 75 additions & 0 deletions examples/affine/raster_pixel_to_world/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# ---------------------------------------------------------------------
# The classic GIS use case: mapping raster pixels to world coordinates
# using a GDAL-style geotransform, then mapping back with the inverse.
# ---------------------------------------------------------------------

heading("Pixel ↔ world coordinates with from_gdal")
note(
"Georeferenced rasters carry a 6-number GDAL geotransform: "
"<code>(x_origin, x_pixel_size, x_skew, y_origin, y_skew, "
"y_pixel_size)</code>. <code>Affine.from_gdal</code> turns that "
"into an Affine you can use to translate between (column, row) "
"pixel indices and (x, y) world coordinates."
)

# A made-up geotransform: 425-meter pixels, north-up, with origin
# in the upper-left (note the negative y pixel size).
geotransform = (-237481.5, 425.0, 0.0, 237536.4, 0.0, -425.0)
pixel_to_world = Affine.from_gdal(*geotransform)
world_to_pixel = ~pixel_to_world

note("Forward transform (pixel → world):")
display(pixel_to_world, append=True)

# A few points of interest on a 200x200 raster.
points_of_interest = {
"upper-left corner": (0, 0),
"center": (100, 100),
"lower-right corner": (200, 200),
}

note("Pixel centers mapped to world coordinates:")
for label, (col, row) in points_of_interest.items():
world_x, world_y = pixel_to_world * (col + 0.5, row + 0.5)
note(
f"<strong>{label}</strong> pixel ({col}, {row}) → "
f"world ({world_x:.1f}, {world_y:.1f})"
)

# Going the other way: which pixel contains a given world coordinate?
target_world = (-150_000.0, 100_000.0)
col_f, row_f = world_to_pixel * target_world
note(
f"World point {target_world} falls in pixel "
f"(col={int(col_f)}, row={int(row_f)})."
)

# Visualize the raster footprint and the points we computed.
fig, ax = plt.subplots(figsize=(7, 6))

# Outline of the full 200x200 raster in world coordinates.
corners_pixel = [(0, 0), (200, 0), (200, 200), (0, 200), (0, 0)]
corners_world = [pixel_to_world * c for c in corners_pixel]
fx, fy = zip(*corners_world)
ax.plot(fx, fy, color="black", linewidth=1.5, label="Raster footprint")
ax.fill(fx, fy, color="lightyellow", alpha=0.5)

# Plot the points of interest.
for label, (col, row) in points_of_interest.items():
wx, wy = pixel_to_world * (col + 0.5, row + 0.5)
ax.plot(wx, wy, "o", markersize=8)
ax.annotate(label, (wx, wy), xytext=(8, 8),
textcoords="offset points", fontsize=9)

# And the target world point.
ax.plot(*target_world, "x", color="red", markersize=12,
markeredgewidth=2, label="Target world point")

ax.set_aspect("equal")
ax.set_xlabel("World X (m)")
ax.set_ylabel("World Y (m)")
ax.set_title("200×200 raster footprint in world coordinates")
ax.legend(loc="lower left")
ax.grid(True, linestyle=":", alpha=0.6)
fig.tight_layout()
display(fig, append=True)
1 change: 1 addition & 0 deletions examples/affine/raster_pixel_to_world/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["affine", "matplotlib"]
21 changes: 21 additions & 0 deletions examples/affine/raster_pixel_to_world/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Lightweight setup for a later notebook cell."""
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)


from affine import Affine
import matplotlib.pyplot as plt
58 changes: 58 additions & 0 deletions examples/affine/transform_basics/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
A first look at the `affine` package: building 2D affine transforms
and applying them to points.

An Affine matrix represents a 2D linear transformation followed by a
translation. You compose them with `*`, apply them to points with `*`,
and invert them with `~`. Documentation: https://github.com/rasterio/affine
"""
from IPython.core.display import display, HTML
from affine import Affine

heading("1. Building transforms with the class methods")
note(
"The most common way to make an Affine is via one of the named "
"constructors: <code>identity</code>, <code>translation</code>, "
"<code>scale</code>, <code>rotation</code> (degrees), and "
"<code>shear</code>. Printing one shows the top two rows of the "
"augmented 3x3 matrix."
)

identity = Affine.identity()
shift = Affine.translation(10.0, 5.0)
zoom = Affine.scale(2.0)
spin = Affine.rotation(30.0) # 30 degrees, counter-clockwise

for name, transform in [
("identity", identity),
("translation(10, 5)", shift),
("scale(2.0)", zoom),
("rotation(30°)", spin),
]:
note(f"<strong>{name}</strong>")
display(transform, append=True)

heading("2. Applying a transform to a point")
note(
"Multiplying an Affine by an <code>(x, y)</code> tuple gives the "
"transformed point. Here we move the point (1, 1) by (10, 5)."
)
moved = shift * (1.0, 1.0)
note(f"shift * (1, 1) = {moved}")

heading("3. Composing and inverting transforms")
note(
"Transforms compose with <code>*</code>. Read right-to-left: the "
"rightmost transform is applied first. The inverse is written "
"<code>~</code>, and a transform times its inverse is the identity."
)
spin_then_shift = shift * spin
note("shift * spin (rotate first, then translate):")
display(spin_then_shift, append=True)

inverse = ~spin_then_shift
roundtrip = inverse * spin_then_shift * (3.0, 4.0)
note(
f"Round-trip of (3, 4) through the transform and its inverse: "
f"({roundtrip[0]:.6f}, {roundtrip[1]:.6f})"
)
1 change: 1 addition & 0 deletions examples/affine/transform_basics/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["affine", "matplotlib"]
40 changes: 40 additions & 0 deletions examples/affine/transform_basics/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Shim setup for the first example. Includes the full IPython shim."""
import sys
import types
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__,
)


ipython = types.ModuleType("IPython")
core = types.ModuleType("IPython.core")
core_display = types.ModuleType("IPython.core.display")
core_display.display = display
core_display.HTML = HTML
ipython.core = core
core.display = core_display
ipython.get_ipython = lambda: None
ipython.display = core_display
sys.modules["IPython"] = ipython
sys.modules["IPython.core"] = core
sys.modules["IPython.core.display"] = core_display
sys.modules["IPython.display"] = core_display


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)


from affine import Affine
import matplotlib.pyplot as plt
59 changes: 59 additions & 0 deletions examples/affine/transforming_a_shape/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# ---------------------------------------------------------------------
# Visualizing how a composed transform reshapes a polygon.
# ---------------------------------------------------------------------

heading("Transforming a house-shaped polygon")
note(
"We define a small polygon resembling a house, then build a "
"composed transform that scales it, rotates it, and shifts it. "
"Applying the transform is just a list comprehension over the "
"vertices."
)

# Vertices of a simple house outline (closed polygon).
house = [
(0.0, 0.0),
(2.0, 0.0),
(2.0, 1.5),
(1.0, 2.5),
(0.0, 1.5),
(0.0, 0.0),
]

# Compose: scale up, rotate 25 degrees, then translate to (4, 1).
# Remember: the rightmost transform is applied first.
transform = (
Affine.translation(4.0, 1.0)
* Affine.rotation(25.0)
* Affine.scale(1.5)
)

note("The composed transform:")
display(transform, append=True)

# Apply the transform to every vertex.
transformed = [transform * point for point in house]

note("First three transformed vertices:")
for original, new in list(zip(house, transformed))[:3]:
nx, ny = new
note(f"({original[0]}, {original[1]}) → ({nx:.3f}, {ny:.3f})")

# Plot the original and transformed polygons side by side.
fig, ax = plt.subplots(figsize=(7, 5))

orig_x, orig_y = zip(*house)
new_x, new_y = zip(*transformed)

ax.fill(orig_x, orig_y, alpha=0.3, color="steelblue", label="Original")
ax.plot(orig_x, orig_y, color="steelblue", linewidth=2)

ax.fill(new_x, new_y, alpha=0.3, color="darkorange", label="Transformed")
ax.plot(new_x, new_y, color="darkorange", linewidth=2)

ax.set_aspect("equal")
ax.grid(True, linestyle=":", alpha=0.6)
ax.set_title("Scale 1.5x, rotate 25°, translate to (4, 1)")
ax.legend(loc="upper left")
fig.tight_layout()
display(fig, append=True)
1 change: 1 addition & 0 deletions examples/affine/transforming_a_shape/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["affine", "matplotlib"]
21 changes: 21 additions & 0 deletions examples/affine/transforming_a_shape/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Lightweight setup for a later notebook cell."""
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)


from affine import Affine
import matplotlib.pyplot as plt