diff --git a/examples/affine/README.md b/examples/affine/README.md
new file mode 100644
index 0000000..3489eea
--- /dev/null
+++ b/examples/affine/README.md
@@ -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.
diff --git a/examples/affine/order.json b/examples/affine/order.json
new file mode 100644
index 0000000..f30e1aa
--- /dev/null
+++ b/examples/affine/order.json
@@ -0,0 +1,5 @@
+[
+ "transform_basics",
+ "transforming_a_shape",
+ "raster_pixel_to_world"
+]
diff --git a/examples/affine/raster_pixel_to_world/code.py b/examples/affine/raster_pixel_to_world/code.py
new file mode 100644
index 0000000..17b791e
--- /dev/null
+++ b/examples/affine/raster_pixel_to_world/code.py
@@ -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: "
+ "(x_origin, x_pixel_size, x_skew, y_origin, y_skew, "
+ "y_pixel_size). Affine.from_gdal 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"{label} 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)
diff --git a/examples/affine/raster_pixel_to_world/config.toml b/examples/affine/raster_pixel_to_world/config.toml
new file mode 100644
index 0000000..13c4e27
--- /dev/null
+++ b/examples/affine/raster_pixel_to_world/config.toml
@@ -0,0 +1 @@
+packages = ["affine", "matplotlib"]
diff --git a/examples/affine/raster_pixel_to_world/setup.py b/examples/affine/raster_pixel_to_world/setup.py
new file mode 100644
index 0000000..6586ca4
--- /dev/null
+++ b/examples/affine/raster_pixel_to_world/setup.py
@@ -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"
{text}
"), append=True) + + +from affine import Affine +import matplotlib.pyplot as plt diff --git a/examples/affine/transform_basics/code.py b/examples/affine/transform_basics/code.py new file mode 100644 index 0000000..c0c1ef1 --- /dev/null +++ b/examples/affine/transform_basics/code.py @@ -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:identity, translation, "
+ "scale, rotation (degrees), and "
+ "shear. 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"{name}")
+ display(transform, append=True)
+
+heading("2. Applying a transform to a point")
+note(
+ "Multiplying an Affine by an (x, y) 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 *. Read right-to-left: the "
+ "rightmost transform is applied first. The inverse is written "
+ "~, 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})"
+)
diff --git a/examples/affine/transform_basics/config.toml b/examples/affine/transform_basics/config.toml
new file mode 100644
index 0000000..13c4e27
--- /dev/null
+++ b/examples/affine/transform_basics/config.toml
@@ -0,0 +1 @@
+packages = ["affine", "matplotlib"]
diff --git a/examples/affine/transform_basics/setup.py b/examples/affine/transform_basics/setup.py
new file mode 100644
index 0000000..a31c46d
--- /dev/null
+++ b/examples/affine/transform_basics/setup.py
@@ -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"{text}
"), append=True) + + +from affine import Affine +import matplotlib.pyplot as plt diff --git a/examples/affine/transforming_a_shape/code.py b/examples/affine/transforming_a_shape/code.py new file mode 100644 index 0000000..d408f98 --- /dev/null +++ b/examples/affine/transforming_a_shape/code.py @@ -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) diff --git a/examples/affine/transforming_a_shape/config.toml b/examples/affine/transforming_a_shape/config.toml new file mode 100644 index 0000000..13c4e27 --- /dev/null +++ b/examples/affine/transforming_a_shape/config.toml @@ -0,0 +1 @@ +packages = ["affine", "matplotlib"] diff --git a/examples/affine/transforming_a_shape/setup.py b/examples/affine/transforming_a_shape/setup.py new file mode 100644 index 0000000..6586ca4 --- /dev/null +++ b/examples/affine/transforming_a_shape/setup.py @@ -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"{text}
"), append=True) + + +from affine import Affine +import matplotlib.pyplot as plt