|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Development Commands |
| 6 | + |
| 7 | +### Testing |
| 8 | +Run all tests: |
| 9 | +```bash |
| 10 | +julia --project=. -e 'using Pkg; Pkg.test()' |
| 11 | +``` |
| 12 | + |
| 13 | +Note: There is currently no way to run a single test file in isolation. |
| 14 | + |
| 15 | +### Git Commit Style |
| 16 | +Commit messages in this repository follow a simple, descriptive style: |
| 17 | + |
| 18 | +- **Use imperative mood**: "Fix bug" not "Fixed bug" or "Fixes bug" |
| 19 | +- **Start with a capital letter**: "Add feature" not "add feature" |
| 20 | +- **Be concise but descriptive**: Explain what changed, not why (unless non-obvious) |
| 21 | +- **No trailing periods**: Commit messages don't end with a period |
| 22 | +- **Use backticks for code**: Reference functions/types with backticks like `smooth` or `TraitTarget` |
| 23 | +- **No conventional commit prefixes**: Don't use "feat:", "fix:", "docs:", etc. |
| 24 | + |
| 25 | +Examples from the project: |
| 26 | +``` |
| 27 | +Fix type constraint in _smooth function |
| 28 | +Add a `smooth` function |
| 29 | +Refactor tests to be a bit easier to parse |
| 30 | +Tree based acceleration for polygon clipping / boolean ops |
| 31 | +Bump version from 0.1.30 to 0.1.31 |
| 32 | +``` |
| 33 | + |
| 34 | +## High-Level Architecture |
| 35 | + |
| 36 | +### Monorepo Structure |
| 37 | +GeometryOps uses a monorepo structure with GeometryOpsCore as a subpackage: |
| 38 | +- **GeometryOpsCore/**: Core abstractions, types, and primitive functions (`apply`, `applyreduce`, `flatten`, etc.) |
| 39 | +- **src/**: Main package implementation |
| 40 | +- **ext/**: Package extensions for optional dependencies (LibGEOS, Proj, TGGeometry, DataFrames, FlexiJoins) |
| 41 | + |
| 42 | +### Core Abstractions |
| 43 | + |
| 44 | +**GeoInterface Integration**: All functions work with any GeoInterface-compatible geometry. Dispatch is based on GeoInterface traits (PointTrait, LineStringTrait, PolygonTrait, etc.), not concrete types. |
| 45 | + |
| 46 | +**Manifold System**: Operations can be performed on different manifolds: |
| 47 | +- `Planar()`: Euclidean/Cartesian coordinates (default) |
| 48 | +- `Spherical()`: Spherical coordinates on a unit sphere |
| 49 | +- `Geodesic()`: Geodesic calculations on Earth (requires Proj extension) |
| 50 | +- `AutoManifold()`: Automatically select appropriate manifold |
| 51 | + |
| 52 | +Functions typically accept a manifold as the first argument: |
| 53 | +```julia |
| 54 | +area(Planar(), polygon) |
| 55 | +area(Spherical(), polygon) |
| 56 | +``` |
| 57 | + |
| 58 | +**Apply Framework**: The `apply` and `applyreduce` functions from GeometryOpsCore are the workhorses for geometry operations: |
| 59 | +- `apply`: Applies a function to geometries matching a target trait, then reconstructs the geometry |
| 60 | +- `applyreduce`: Applies a function and reduces the results (e.g., sum, min, max) |
| 61 | +- `TraitTarget`: Specifies which geometry traits to target (e.g., `TraitTarget{GI.PointTrait}()`) |
| 62 | + |
| 63 | +Example pattern: |
| 64 | +```julia |
| 65 | +applyreduce(WithTrait((trait, g) -> _area(T, trait, g)), +, _AREA_TARGETS, geom; threaded, init=zero(T)) |
| 66 | +``` |
| 67 | + |
| 68 | +### Code Organization Principles |
| 69 | + |
| 70 | +1. **Literate Programming**: Source files use literate programming with documentation and examples at the top, followed by implementation. Examples should include visual plots using Makie/CairoMakie when appropriate. |
| 71 | + |
| 72 | +2. **One File, One Job**: Each file should handle one semantic concept (e.g., `area.jl`, `distance.jl`, `centroid.jl`). Common utilities can be extracted to separate files. |
| 73 | + |
| 74 | +3. **Public vs Internal**: |
| 75 | + - Public functions: Exported, documented, promise API stability |
| 76 | + - Internal functions: Prefixed with `_`, not exported, may have comments instead of docstrings |
| 77 | + |
| 78 | +4. **File Structure Pattern** (see `src/methods/area.jl` or `src/methods/distance.jl`): |
| 79 | + ```julia |
| 80 | + # # Title |
| 81 | + export function_name |
| 82 | + |
| 83 | + #= |
| 84 | + ## What is [concept]? |
| 85 | + [Explanation with examples, plots] |
| 86 | +
|
| 87 | + ## Implementation |
| 88 | + [Implementation notes] |
| 89 | + =# |
| 90 | + |
| 91 | + # Public API with docstring |
| 92 | + function_name(geom, args...) = ... |
| 93 | + |
| 94 | + # Internal implementation functions |
| 95 | + _function_name(...) = ... |
| 96 | + ``` |
| 97 | + |
| 98 | +### Directory Structure |
| 99 | + |
| 100 | +- **src/methods/**: Geometric operations and predicates |
| 101 | + - Basic: `area.jl`, `centroid.jl`, `distance.jl`, `perimeter.jl`, `angles.jl` |
| 102 | + - Spatial relations: `geom_relations/` (contains, intersects, within, etc.) |
| 103 | + - Clipping: `clipping/` (intersection, union, difference, cut, coverage) |
| 104 | + - Other: `barycentric.jl`, `buffer.jl`, `convex_hull.jl`, `orientation.jl`, `polygonize.jl` |
| 105 | + |
| 106 | +- **src/transformations/**: Geometry transformations |
| 107 | + - `simplify.jl`, `segmentize.jl`, `smooth.jl`, `flip.jl`, `transform.jl` |
| 108 | + - `reproject.jl`: Coordinate reference system transformations |
| 109 | + - `correction/`: Geometry correction utilities |
| 110 | + |
| 111 | +- **src/utils/**: Utility modules |
| 112 | + - `LoopStateMachine/`: State machine for polygon processing |
| 113 | + - `SpatialTreeInterface/`: Spatial indexing (STRtree) |
| 114 | + - `UnitSpherical/`: Spherical geometry utilities |
| 115 | + - `NaturalIndexing.jl`: Natural indexing utilities |
| 116 | + |
| 117 | +- **test/**: Mirror structure of src/ with corresponding test files |
| 118 | + |
| 119 | +## Writing a New Algorithm |
| 120 | + |
| 121 | +### Step-by-Step Pattern |
| 122 | + |
| 123 | +1. **Choose the right location**: |
| 124 | + - Methods (area, distance, etc.) go in `src/methods/` |
| 125 | + - Transformations (simplify, flip, etc.) go in `src/transformations/` |
| 126 | + |
| 127 | +2. **Create the file with literate documentation**: |
| 128 | +```julia |
| 129 | +# # Algorithm Name |
| 130 | +export algorithm_name |
| 131 | + |
| 132 | +#= |
| 133 | +## What is [algorithm]? |
| 134 | +[Clear explanation with visual examples using Makie] |
| 135 | +
|
| 136 | +```@example demo |
| 137 | +import GeometryOps as GO |
| 138 | +import GeoInterface as GI |
| 139 | +using CairoMakie |
| 140 | +
|
| 141 | +# Create example geometry and demonstrate usage |
| 142 | +polygon = GI.Polygon([[(0,0), (1,0), (1,1), (0,1), (0,0)]]) |
| 143 | +result = GO.algorithm_name(polygon) |
| 144 | +``` |
| 145 | +
|
| 146 | +## Implementation |
| 147 | +[Notes about the approach, algorithm complexity, special cases] |
| 148 | +=# |
| 149 | +``` |
| 150 | + |
| 151 | +3. **Define target traits** (if using apply/applyreduce): |
| 152 | +```julia |
| 153 | +const _ALGORITHM_TARGETS = TraitTarget{Union{GI.PolygonTrait,GI.MultiPolygonTrait}}() |
| 154 | +``` |
| 155 | + |
| 156 | +4. **Implement public API** with manifold support: |
| 157 | +```julia |
| 158 | +""" |
| 159 | + algorithm_name(geom, [T = Float64]; kwargs...) |
| 160 | +
|
| 161 | +[Docstring with description, parameters, return value] |
| 162 | +""" |
| 163 | +function algorithm_name(geom, ::Type{T} = Float64; threaded=false, kwargs...) where T |
| 164 | + algorithm_name(Planar(), geom, T; threaded, kwargs...) |
| 165 | +end |
| 166 | + |
| 167 | +# Manifold-specific implementations |
| 168 | +function algorithm_name(m::Planar, geom, ::Type{T} = Float64; threaded=false, kwargs...) where T |
| 169 | + # Use apply or applyreduce pattern |
| 170 | + applyreduce(WithTrait((trait, g) -> _algorithm(T, trait, g)), +, _ALGORITHM_TARGETS, geom; threaded, init=zero(T), kwargs...) |
| 171 | +end |
| 172 | + |
| 173 | +function algorithm_name(m::Spherical, geom, ::Type{T} = Float64; threaded=false, kwargs...) where T |
| 174 | + # Spherical implementation if applicable |
| 175 | +end |
| 176 | +``` |
| 177 | + |
| 178 | +5. **Implement internal functions** (trait-dispatched): |
| 179 | +```julia |
| 180 | +# Dispatch on different geometry traits |
| 181 | +_algorithm(::Type{T}, ::GI.PointTrait, point) where T = # Point implementation |
| 182 | +_algorithm(::Type{T}, ::GI.LineStringTrait, linestring) where T = # LineString implementation |
| 183 | +_algorithm(::Type{T}, ::GI.PolygonTrait, polygon) where T = # Polygon implementation |
| 184 | +``` |
| 185 | + |
| 186 | +6. **Add to main module**: Include the file in `src/GeometryOps.jl`: |
| 187 | +```julia |
| 188 | +include("methods/algorithm_name.jl") |
| 189 | +``` |
| 190 | + |
| 191 | +7. **Write tests**: Create `test/methods/algorithm_name.jl` mirroring the source structure: |
| 192 | +```julia |
| 193 | +using Test |
| 194 | +using GeometryOps |
| 195 | +import GeoInterface as GI |
| 196 | + |
| 197 | +@testset "Algorithm Name" begin |
| 198 | + @testset "Point" begin |
| 199 | + # Test with points |
| 200 | + end |
| 201 | + |
| 202 | + @testset "Polygon" begin |
| 203 | + # Test with polygons |
| 204 | + end |
| 205 | + |
| 206 | + @testset "Edge cases" begin |
| 207 | + # Empty geometries, degenerate cases, etc. |
| 208 | + end |
| 209 | +end |
| 210 | +``` |
| 211 | + |
| 212 | +8. **Register test**: Add to `test/runtests.jl`: |
| 213 | +```julia |
| 214 | +@safetestset "Algorithm Name" begin include("methods/algorithm_name.jl") end |
| 215 | +``` |
| 216 | + |
| 217 | +## Interfacing with Input |
| 218 | + |
| 219 | +### GeoInterface Pattern |
| 220 | +All functions accept any GeoInterface-compatible geometry. Always use GeoInterface accessors: |
| 221 | +```julia |
| 222 | +GI.trait(geom) # Get geometry trait |
| 223 | +GI.npoint(geom) # Number of points |
| 224 | +GI.getpoint(geom, i) # Get point at index i |
| 225 | +GI.getpoint(geom) # Iterator over points |
| 226 | +GI.x(point) # X coordinate |
| 227 | +GI.y(point) # Y coordinate |
| 228 | +GI.z(point) # Z coordinate (if exists) |
| 229 | +GI.getexterior(poly) # Exterior ring |
| 230 | +GI.gethole(poly) # Iterator over holes |
| 231 | +GI.isempty(geom) # Check if empty |
| 232 | +``` |
| 233 | + |
| 234 | +### Type Flexibility |
| 235 | +Functions should accept a type parameter `T` (defaulting to `Float64`) for numeric calculations: |
| 236 | +```julia |
| 237 | +function my_function(geom, ::Type{T} = Float64) where T <: AbstractFloat |
| 238 | + result = zero(T) |
| 239 | + # ... computation using type T |
| 240 | + return result |
| 241 | +end |
| 242 | +``` |
| 243 | + |
| 244 | +### Threading Support |
| 245 | +Most operations support threading via `threaded=false` keyword. Threading happens at the highest level (over arrays, feature collections, or multi-geometries): |
| 246 | +```julia |
| 247 | +applyreduce(f, op, targets, geom; threaded=true) # Enable threading |
| 248 | +``` |
| 249 | + |
| 250 | +### Extension-Based Optional Features |
| 251 | +Optional dependencies are loaded via extensions: |
| 252 | +- **GEOS**: `GEOS()` algorithm for calling LibGEOS functions |
| 253 | +- **Proj**: `PROJ()` algorithm for reprojection and geodesic operations |
| 254 | +- **TGGeometry**: `TG()` algorithm for fast C-based predicates |
| 255 | +- **DataFrames**: `apply` works on DataFrame columns |
| 256 | +- **FlexiJoins**: Spatial join operations |
| 257 | + |
| 258 | +Use algorithm types to dispatch to these implementations. **The algorithm is always the first argument:** |
| 259 | +```julia |
| 260 | +# Use GEOS implementation (algorithm comes first) |
| 261 | +buffer(GEOS(), geom, 10.0) |
| 262 | + |
| 263 | +# Use native Julia implementation (default) |
| 264 | +buffer(geom, 10.0) |
| 265 | +``` |
| 266 | + |
| 267 | +The `Algorithm` type and its subtypes are defined in `GeometryOpsCore/src/types/algorithm.jl`. |
| 268 | + |
| 269 | +### Working with Tables |
| 270 | +`apply` and `applyreduce` work with any Tables.jl-compatible table (DataFrames, etc.): |
| 271 | +```julia |
| 272 | +# Apply operation to geometry column in a table |
| 273 | +result = apply(PointTrait(), df.geometry) do point |
| 274 | + (GI.y(point), GI.x(point)) # Flip coordinates |
| 275 | +end |
| 276 | +``` |
| 277 | + |
| 278 | +## Common Patterns |
| 279 | + |
| 280 | +### Error Handling for Missing Extensions |
| 281 | +Functions requiring extensions should provide helpful error hints: |
| 282 | +```julia |
| 283 | +function __init__() |
| 284 | + Base.Experimental.register_error_hint(_reproject_error_hinter, MethodError) |
| 285 | + Base.Experimental.register_error_hint(_geodesic_segments_error_hinter, MethodError) |
| 286 | + Base.Experimental.register_error_hint(_buffer_error_hinter, MethodError) |
| 287 | +end |
| 288 | +``` |
| 289 | + |
| 290 | +### TraitTarget Usage |
| 291 | +Use `TraitTarget` to specify which geometry types your function operates on: |
| 292 | +```julia |
| 293 | +# Single trait |
| 294 | +const _TARGETS = TraitTarget{GI.PointTrait}() |
| 295 | + |
| 296 | +# Union of traits |
| 297 | +const _TARGETS = TraitTarget{Union{GI.PolygonTrait,GI.MultiPolygonTrait}}() |
| 298 | + |
| 299 | +# More complex unions |
| 300 | +const _TARGETS = TraitTarget{Union{GI.PointTrait,GI.LineStringTrait,GI.LinearRingTrait}}() |
| 301 | +``` |
| 302 | + |
| 303 | +### WithTrait Pattern |
| 304 | +Use `WithTrait` wrapper when you need both the trait and geometry in your function: |
| 305 | +```julia |
| 306 | +applyreduce(WithTrait((trait, geom) -> _my_function(trait, geom)), +, targets, input) |
| 307 | +``` |
| 308 | + |
| 309 | +### Handling Empty Geometries |
| 310 | +Always check for empty geometries when appropriate: |
| 311 | +```julia |
| 312 | +GI.isempty(geom) && return zero(T) # Or appropriate default value |
| 313 | +``` |
0 commit comments