isolines 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- isolines-0.1.0/CHANGELOG.md +27 -0
- isolines-0.1.0/CITATION.cff +21 -0
- isolines-0.1.0/LICENSE +21 -0
- isolines-0.1.0/MANIFEST.in +9 -0
- isolines-0.1.0/PKG-INFO +269 -0
- isolines-0.1.0/README.md +228 -0
- isolines-0.1.0/examples/quickstart.py +74 -0
- isolines-0.1.0/pyproject.toml +122 -0
- isolines-0.1.0/setup.cfg +4 -0
- isolines-0.1.0/src/isolines/__init__.py +74 -0
- isolines-0.1.0/src/isolines/__main__.py +5 -0
- isolines-0.1.0/src/isolines/_version.py +1 -0
- isolines-0.1.0/src/isolines/cli.py +172 -0
- isolines-0.1.0/src/isolines/contours.py +342 -0
- isolines-0.1.0/src/isolines/geometry.py +311 -0
- isolines-0.1.0/src/isolines/interpolate.py +227 -0
- isolines-0.1.0/src/isolines/io.py +197 -0
- isolines-0.1.0/src/isolines/levels.py +54 -0
- isolines-0.1.0/src/isolines/marching.py +322 -0
- isolines-0.1.0/src/isolines/py.typed +0 -0
- isolines-0.1.0/src/isolines/transform.py +108 -0
- isolines-0.1.0/src/isolines.egg-info/PKG-INFO +269 -0
- isolines-0.1.0/src/isolines.egg-info/SOURCES.txt +29 -0
- isolines-0.1.0/src/isolines.egg-info/dependency_links.txt +1 -0
- isolines-0.1.0/src/isolines.egg-info/entry_points.txt +2 -0
- isolines-0.1.0/src/isolines.egg-info/requires.txt +14 -0
- isolines-0.1.0/src/isolines.egg-info/top_level.txt +1 -0
- isolines-0.1.0/tests/test_contours.py +348 -0
- isolines-0.1.0/tests/test_geometry.py +138 -0
- isolines-0.1.0/tests/test_interpolate.py +91 -0
- isolines-0.1.0/tests/test_io_cli.py +177 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-09
|
|
8
|
+
### Added
|
|
9
|
+
- `contour_lines`: marching squares with linear interpolation along cell edges and
|
|
10
|
+
centre-average saddle disambiguation; segments joined into continuous, consistently
|
|
11
|
+
oriented LineStrings (higher values on the left) with closed-ring detection; level and
|
|
12
|
+
label attributes; minimum-length filter.
|
|
13
|
+
- `isobands`: filled contours as MultiPolygons with holes, traced from oriented contour
|
|
14
|
+
segments and the boundary of the valid grid domain, with correct nesting (islands in
|
|
15
|
+
holes) and RFC 7946 orientation; open-ended bands with `-inf`/`inf`; bands tile the valid
|
|
16
|
+
domain without gaps or overlaps.
|
|
17
|
+
- No-data handling (NaN, masked arrays, `nodata` value); domain pinches split into valid
|
|
18
|
+
rings.
|
|
19
|
+
- Georeferencing by GDAL-style `GeoTransform` (or rasterio `Affine`, or 1-D x/y
|
|
20
|
+
coordinates), pixel-is-area or pixel-is-point.
|
|
21
|
+
- Chaikin smoothing and Douglas-Peucker simplification; `validate_polygon` checker.
|
|
22
|
+
- Scattered points to grid: IDW (power, radius, k nearest), nearest neighbour, and linear
|
|
23
|
+
TIN interpolation via the optional `scipy` extra; `GridSpec`.
|
|
24
|
+
- Level helpers: `interval_levels`, `nice_interval`, `nice_levels`.
|
|
25
|
+
- I/O: Esri ASCII grid read/write, `.npy`, any raster through the optional `raster`
|
|
26
|
+
extra (rasterio), point CSV reader, GeoJSON writer with coordinate rounding.
|
|
27
|
+
- CLI: `isolines lines`, `isolines bands`, `isolines grid`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
authors:
|
|
4
|
+
- family-names: "Eminoğlu"
|
|
5
|
+
given-names: "Yusuf"
|
|
6
|
+
email: "geospacephilo@gmail.com"
|
|
7
|
+
orcid: "https://orcid.org/0009-0005-6000-2934"
|
|
8
|
+
title: "isolines"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-09"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://gitlab.com/geophilo1/isolines_sdk"
|
|
13
|
+
url: "https://pypi.org/project/isolines/"
|
|
14
|
+
abstract: "Contour lines and filled contours (isobands) from gridded data and scattered points: marching squares with saddle disambiguation and linear interpolation, joined LineStrings with closed-ring detection, valid RFC 7946 polygons with holes, Chaikin smoothing, Douglas-Peucker simplification, IDW/nearest/TIN gridding, affine georeferencing and GeoJSON output, with numpy as the only dependency."
|
|
15
|
+
keywords:
|
|
16
|
+
- contours
|
|
17
|
+
- isolines
|
|
18
|
+
- isobands
|
|
19
|
+
- marching squares
|
|
20
|
+
- interpolation
|
|
21
|
+
- gis
|
isolines-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuf Eminoğlu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
isolines-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: isolines
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Contour lines and filled contours (isobands) from grids and scattered points: marching squares with saddle disambiguation, valid polygons with holes, smoothing, simplification, IDW/TIN gridding and GeoJSON output, numpy only.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/geophilo1/isolines_sdk
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/geophilo1/isolines_sdk
|
|
9
|
+
Project-URL: Issues, https://gitlab.com/geophilo1/isolines_sdk/-/work_items
|
|
10
|
+
Project-URL: Changelog, https://gitlab.com/geophilo1/isolines_sdk/-/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: contour,contours,isolines,isobands,marching-squares,filled-contours,dem,isochrone,noise-map,idw,interpolation,geojson,gis,raster
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Environment :: Console
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
23
|
+
Classifier: Intended Audience :: Science/Research
|
|
24
|
+
Classifier: Intended Audience :: Developers
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: numpy>=1.21
|
|
30
|
+
Provides-Extra: scipy
|
|
31
|
+
Requires-Dist: scipy>=1.7; extra == "scipy"
|
|
32
|
+
Provides-Extra: raster
|
|
33
|
+
Requires-Dist: rasterio>=1.3; extra == "raster"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
38
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: shapely>=2.0; extra == "dev"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# isolines
|
|
43
|
+
|
|
44
|
+
[](https://pypi.org/project/isolines/)
|
|
45
|
+
[](https://pypi.org/project/isolines/)
|
|
46
|
+
[](https://gitlab.com/geophilo1/isolines_sdk/-/pipelines)
|
|
47
|
+
[](https://gitlab.com/geophilo1/isolines_sdk/-/blob/main/LICENSE)
|
|
48
|
+
|
|
49
|
+
**Contour lines and filled contours (isobands) from grids and scattered points, numpy only.**
|
|
50
|
+
|
|
51
|
+
`isolines` turns a 2-D grid (a DEM, a travel-time surface, a noise
|
|
52
|
+
raster) or a set of scattered measurements into:
|
|
53
|
+
|
|
54
|
+
- **contour lines**: continuous `LineString`s, one per connected piece, with
|
|
55
|
+
closed-ring detection, `level` and `label` attributes, and a consistent
|
|
56
|
+
direction (higher values on the left);
|
|
57
|
+
- **isobands**: `MultiPolygon`s with holes for each value interval, with
|
|
58
|
+
correct nesting (islands inside holes inside polygons), RFC 7946 ring
|
|
59
|
+
orientation, and bands that tile the valid grid without gaps or overlaps.
|
|
60
|
+
|
|
61
|
+
It handles no-data (NaN, masked arrays, a `nodata` value), georeferences
|
|
62
|
+
through an affine geotransform, smooths (Chaikin) and simplifies
|
|
63
|
+
(Douglas-Peucker), interpolates scattered points to a grid (IDW, nearest,
|
|
64
|
+
linear TIN), and writes GeoJSON. Its only dependency is numpy.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install isolines # numpy only
|
|
68
|
+
pip install "isolines[scipy]" # + linear (Delaunay TIN) interpolation
|
|
69
|
+
pip install "isolines[raster]" # + GeoTIFF and other rasters through rasterio
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Requires Python 3.9 or newer.
|
|
73
|
+
|
|
74
|
+
Typical uses: DEM contours, travel-time isochrones from sampled network
|
|
75
|
+
times, noise maps from receiver levels, any "contour this surface" job
|
|
76
|
+
where you don't want a GDAL install.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Quick start (Python)
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import math
|
|
84
|
+
import numpy as np
|
|
85
|
+
import isolines
|
|
86
|
+
|
|
87
|
+
# A gridded surface with georeferencing (GDAL-style, north-up, 25 m cells)
|
|
88
|
+
yy, xx = np.mgrid[0:200, 0:300]
|
|
89
|
+
dem = 50 + 40 * np.exp(-((xx - 150) ** 2 + (yy - 100) ** 2) / 2000)
|
|
90
|
+
gt = isolines.GeoTransform.north_up(500000.0, 4200000.0, 25.0)
|
|
91
|
+
|
|
92
|
+
levels = isolines.interval_levels(dem.min(), dem.max(), 5) # 55, 60, ..., 90
|
|
93
|
+
lines = isolines.contour_lines(dem, levels, transform=gt, smooth=2, label_format="{:g} m")
|
|
94
|
+
bands = isolines.isobands(dem, [-math.inf, 60, 70, 80, math.inf], transform=gt)
|
|
95
|
+
|
|
96
|
+
for band in bands:
|
|
97
|
+
print(band.label, round(band.area / 1e4, 1), "ha")
|
|
98
|
+
|
|
99
|
+
isolines.write_geojson("contours.geojson", lines, precision=2)
|
|
100
|
+
isolines.write_geojson("bands.geojson", bands, precision=2)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
From scattered points (for example travel times at network nodes):
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
z, gt = isolines.points_to_grid(x, y, minutes, cell_size=50.0, method="idw", k=8)
|
|
107
|
+
isochrones = isolines.isobands(z, [0, 5, 10, 15], transform=gt)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
A rasterio dataset can be used directly:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
r = isolines.read_raster("dem.tif") # needs the "raster" extra; NaN for no-data
|
|
114
|
+
lines = isolines.contour_lines(r.z, isolines.nice_levels(r.z), transform=r.transform)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
See [`examples/quickstart.py`](examples/quickstart.py) for a DEM, a noise
|
|
118
|
+
map and an isochrone example.
|
|
119
|
+
|
|
120
|
+
## Quick start (command line)
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# contour lines every 10 m from a GeoTIFF (or an Esri ASCII .asc grid)
|
|
124
|
+
isolines lines dem.tif --interval 10 --smooth 2 -o contours.geojson
|
|
125
|
+
|
|
126
|
+
# filled bands with explicit breaks, open-ended below and above
|
|
127
|
+
isolines bands noise.asc --breaks 55,60,65,70 --open-ends -o noise.geojson
|
|
128
|
+
|
|
129
|
+
# bands straight from a point CSV (interpolated to a 50 m grid first)
|
|
130
|
+
isolines bands times.csv --value-col minutes --cell-size 50 --method idw \
|
|
131
|
+
--breaks 0,5,10,15 -o isochrones.geojson
|
|
132
|
+
|
|
133
|
+
# just the interpolation, written as an Esri ASCII grid
|
|
134
|
+
isolines grid times.csv --value-col minutes --cell-size 50 -o times.asc
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Without `--levels`/`--breaks`/`--interval`, about ten round-numbered levels
|
|
138
|
+
are chosen. `--simplify`, `--precision`, `--label-format`, `--min-length`
|
|
139
|
+
and `--min-area` are available; `isolines <command> --help` lists all
|
|
140
|
+
options.
|
|
141
|
+
|
|
142
|
+
## API overview
|
|
143
|
+
|
|
144
|
+
| Function / class | Purpose |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `contour_lines(z, levels, *, x, y, transform, nodata, pixel_is_area, smooth, simplify, min_length, label_format)` | List of `ContourLine(level, coords, closed, label)` |
|
|
147
|
+
| `isobands(z, breaks, *, ..., min_area, keep_empty)` | List of `IsoBand(lower, upper, polygons, label)`, one per interval `[lower, upper)` |
|
|
148
|
+
| `GeoTransform` / `as_geotransform` | GDAL 6-coefficient transform; accepts GDAL tuples and rasterio `Affine` |
|
|
149
|
+
| `idw`, `nearest`, `linear`, `points_to_grid`, `GridSpec` | Scattered points to grid |
|
|
150
|
+
| `interval_levels`, `nice_interval`, `nice_levels` | Level selection |
|
|
151
|
+
| `chaikin`, `douglas_peucker`, `signed_area`, `line_length`, `point_in_ring` | Geometry helpers |
|
|
152
|
+
| `assemble_polygons`, `orient_polygon`, `validate_polygon` | Polygon building and validity checking |
|
|
153
|
+
| `read_ascii_grid`, `write_ascii_grid`, `read_raster`, `load_grid`, `read_points_csv` | Input |
|
|
154
|
+
| `to_geojson`, `write_geojson`, `.to_feature()` | GeoJSON output |
|
|
155
|
+
|
|
156
|
+
Coordinates: with `transform`, grid value `z[row, col]` is placed at the
|
|
157
|
+
pixel centre (`pixel_is_area=True`, the GDAL/rasterio convention) or the
|
|
158
|
+
pixel corner (`False`). With `x`/`y` (1-D, strictly monotonic), at
|
|
159
|
+
`(x[col], y[row])`. With neither, at `(col, row)`.
|
|
160
|
+
|
|
161
|
+
GeoJSON properties: lines carry `level`, `label`, `closed`, `length`;
|
|
162
|
+
bands carry `lower`, `upper` (`null` for infinite), `label`, `area`.
|
|
163
|
+
|
|
164
|
+
## Method and sources
|
|
165
|
+
|
|
166
|
+
- **Marching squares.** Each grid cell is classified by which corners are
|
|
167
|
+
`>= level` (16 cases); crossing points are placed on cell edges by linear
|
|
168
|
+
interpolation between the two edge nodes. This is the 2-D case of
|
|
169
|
+
Lorensen, W. E. & Cline, H. E. (1987), *Marching cubes: a high resolution
|
|
170
|
+
3D surface construction algorithm*, SIGGRAPH Computer Graphics 21(4),
|
|
171
|
+
163-169; see also Maple, C. (2003), *Geometric design and space planning
|
|
172
|
+
using the marching squares and marching cube algorithms*, Proc.
|
|
173
|
+
GMAG 2003, 90-95.
|
|
174
|
+
- **Saddle disambiguation.** In the two ambiguous cases (diagonal corners
|
|
175
|
+
above), the mean of the four corner values stands in for the value at the
|
|
176
|
+
cell centre: when it is `>= level` the high corners are joined through the
|
|
177
|
+
cell, otherwise the low corners are. (The asymptotic decider of Nielson,
|
|
178
|
+
G. M. & Hamann, B. (1991), *The asymptotic decider*, Proc. IEEE
|
|
179
|
+
Visualization '91, 83-91, is the bilinear-exact alternative; it is not
|
|
180
|
+
used here.)
|
|
181
|
+
- **Joining and orientation.** Every segment is directed so that the
|
|
182
|
+
`>= level` side lies on its left. Each edge crossing then starts exactly
|
|
183
|
+
one segment and ends exactly one, so joining is unambiguous and closed
|
|
184
|
+
rings are detected exactly by shared crossing identities, not by
|
|
185
|
+
coordinate tolerance.
|
|
186
|
+
- **Isobands.** The boundary of the band `lower <= z < upper` is made of
|
|
187
|
+
the oriented contour segments of `lower`, the reversed contour segments of
|
|
188
|
+
`upper`, and the parts of the valid-domain boundary (grid edge and no-data
|
|
189
|
+
cells) whose values fall in the band. These are traced into closed rings;
|
|
190
|
+
counter-clockwise rings are exteriors, clockwise rings are holes and go to
|
|
191
|
+
the smallest exterior containing them. Rings that touch themselves where
|
|
192
|
+
the no-data domain pinches are split, so polygons are valid under the OGC
|
|
193
|
+
Simple Features rules. Output rings follow **RFC 7946, section 3.1.6**
|
|
194
|
+
(exterior counter-clockwise, holes clockwise; first and last positions
|
|
195
|
+
equal).
|
|
196
|
+
- **Exact level values.** A node equal to a level counts as above it.
|
|
197
|
+
Crossing parameters are clamped to `[1e-7, 1 - 1e-7]` of the edge, so a
|
|
198
|
+
contour through a node never produces coincident points on two edges;
|
|
199
|
+
the microscopic rings this would leave around an isolated node equal to
|
|
200
|
+
the level (for example a peak equal to the top level) are dropped.
|
|
201
|
+
- **Smoothing:** Chaikin, G. M. (1974), *An algorithm for high-speed curve
|
|
202
|
+
generation*, Computer Graphics and Image Processing 3(4), 346-349.
|
|
203
|
+
- **Simplification:** Douglas, D. H. & Peucker, T. K. (1973), *Algorithms
|
|
204
|
+
for the reduction of the number of points required to represent a
|
|
205
|
+
digitized line or its caricature*, Cartographica 10(2), 112-122.
|
|
206
|
+
- **IDW:** Shepard, D. (1968), *A two-dimensional interpolation function
|
|
207
|
+
for irregularly-spaced data*, Proc. 23rd ACM National Conference,
|
|
208
|
+
517-524. Weights `d ** -power`; a query on a sample takes its value.
|
|
209
|
+
- **Linear TIN:** Delaunay triangulation and barycentric interpolation via
|
|
210
|
+
`scipy.interpolate.LinearNDInterpolator`; NaN outside the convex hull.
|
|
211
|
+
- **Geotransform:** the GDAL six-coefficient affine model (GDAL Raster Data
|
|
212
|
+
Model documentation). **Esri ASCII grid:** header keys and row order as in
|
|
213
|
+
the Esri "Esri ASCII raster format" documentation.
|
|
214
|
+
|
|
215
|
+
The test suite checks analytic surfaces with known answers: a plane
|
|
216
|
+
(contour position and band area exact), a cone (contours within the
|
|
217
|
+
`1/(8r)` interpolation error of a circle, annulus area), the saddle `z = x*y`
|
|
218
|
+
(contour on the axes, saddle-cell areas computed by hand), nested
|
|
219
|
+
peak/pit/plateau surfaces (island in a hole), no-data holes and pinches,
|
|
220
|
+
plus 40 random integer grids whose values sit exactly on the breaks. Every
|
|
221
|
+
band polygon is validated, and the bands are checked to tile the valid
|
|
222
|
+
domain; shapely (`is_valid`, areas) is used as an independent check when
|
|
223
|
+
installed.
|
|
224
|
+
|
|
225
|
+
## Limitations
|
|
226
|
+
|
|
227
|
+
- Linear interpolation along edges means contours of curved surfaces are
|
|
228
|
+
chords; refine the grid for more accuracy.
|
|
229
|
+
- The saddle rule uses the cell mean, not the bilinear asymptotic decider;
|
|
230
|
+
in rare saddle cells the topology can differ from bilinear interpolation.
|
|
231
|
+
- `smooth` and `simplify` work ring by ring and are not topology-preserving:
|
|
232
|
+
neighbouring bands may slightly overlap or leave slivers, and a smoothed
|
|
233
|
+
line can cross a neighbouring level. Leave them off when exact tiling
|
|
234
|
+
matters.
|
|
235
|
+
- Rounding coordinates (`precision`) can merge points that are only `1e-7`
|
|
236
|
+
cells apart at exact-level nodes; keep enough decimals for your units.
|
|
237
|
+
- `idw` and `nearest` are brute force (chunked numpy), fine for tens of
|
|
238
|
+
thousands of samples; IDW is an exact interpolator but not a statistical
|
|
239
|
+
one (no kriging, no barriers). A noise or travel-time surface interpolated
|
|
240
|
+
from points is only as good as the sample coverage.
|
|
241
|
+
- GeoJSON output carries no CRS (RFC 7946 assumes WGS 84); projected
|
|
242
|
+
coordinates are written as given.
|
|
243
|
+
- Lines and polygons are built in memory; very large grids (10^8 cells)
|
|
244
|
+
need tiling.
|
|
245
|
+
|
|
246
|
+
## Development
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
python -m pip install -e ".[dev,scipy,raster]"
|
|
250
|
+
python -m pytest tests
|
|
251
|
+
python -m ruff check . && python -m ruff format --check src tests
|
|
252
|
+
python -m mypy src
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Academic citation
|
|
256
|
+
|
|
257
|
+
```bibtex
|
|
258
|
+
@software{eminoglu_isolines,
|
|
259
|
+
author = {Eminoğlu, Yusuf},
|
|
260
|
+
title = {isolines},
|
|
261
|
+
year = {2026},
|
|
262
|
+
version = {0.1.0},
|
|
263
|
+
url = {https://gitlab.com/geophilo1/isolines_sdk}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT License. Copyright (c) 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
|
isolines-0.1.0/README.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# isolines
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/isolines/)
|
|
4
|
+
[](https://pypi.org/project/isolines/)
|
|
5
|
+
[](https://gitlab.com/geophilo1/isolines_sdk/-/pipelines)
|
|
6
|
+
[](https://gitlab.com/geophilo1/isolines_sdk/-/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
**Contour lines and filled contours (isobands) from grids and scattered points, numpy only.**
|
|
9
|
+
|
|
10
|
+
`isolines` turns a 2-D grid (a DEM, a travel-time surface, a noise
|
|
11
|
+
raster) or a set of scattered measurements into:
|
|
12
|
+
|
|
13
|
+
- **contour lines**: continuous `LineString`s, one per connected piece, with
|
|
14
|
+
closed-ring detection, `level` and `label` attributes, and a consistent
|
|
15
|
+
direction (higher values on the left);
|
|
16
|
+
- **isobands**: `MultiPolygon`s with holes for each value interval, with
|
|
17
|
+
correct nesting (islands inside holes inside polygons), RFC 7946 ring
|
|
18
|
+
orientation, and bands that tile the valid grid without gaps or overlaps.
|
|
19
|
+
|
|
20
|
+
It handles no-data (NaN, masked arrays, a `nodata` value), georeferences
|
|
21
|
+
through an affine geotransform, smooths (Chaikin) and simplifies
|
|
22
|
+
(Douglas-Peucker), interpolates scattered points to a grid (IDW, nearest,
|
|
23
|
+
linear TIN), and writes GeoJSON. Its only dependency is numpy.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install isolines # numpy only
|
|
27
|
+
pip install "isolines[scipy]" # + linear (Delaunay TIN) interpolation
|
|
28
|
+
pip install "isolines[raster]" # + GeoTIFF and other rasters through rasterio
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires Python 3.9 or newer.
|
|
32
|
+
|
|
33
|
+
Typical uses: DEM contours, travel-time isochrones from sampled network
|
|
34
|
+
times, noise maps from receiver levels, any "contour this surface" job
|
|
35
|
+
where you don't want a GDAL install.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick start (Python)
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import math
|
|
43
|
+
import numpy as np
|
|
44
|
+
import isolines
|
|
45
|
+
|
|
46
|
+
# A gridded surface with georeferencing (GDAL-style, north-up, 25 m cells)
|
|
47
|
+
yy, xx = np.mgrid[0:200, 0:300]
|
|
48
|
+
dem = 50 + 40 * np.exp(-((xx - 150) ** 2 + (yy - 100) ** 2) / 2000)
|
|
49
|
+
gt = isolines.GeoTransform.north_up(500000.0, 4200000.0, 25.0)
|
|
50
|
+
|
|
51
|
+
levels = isolines.interval_levels(dem.min(), dem.max(), 5) # 55, 60, ..., 90
|
|
52
|
+
lines = isolines.contour_lines(dem, levels, transform=gt, smooth=2, label_format="{:g} m")
|
|
53
|
+
bands = isolines.isobands(dem, [-math.inf, 60, 70, 80, math.inf], transform=gt)
|
|
54
|
+
|
|
55
|
+
for band in bands:
|
|
56
|
+
print(band.label, round(band.area / 1e4, 1), "ha")
|
|
57
|
+
|
|
58
|
+
isolines.write_geojson("contours.geojson", lines, precision=2)
|
|
59
|
+
isolines.write_geojson("bands.geojson", bands, precision=2)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
From scattered points (for example travel times at network nodes):
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
z, gt = isolines.points_to_grid(x, y, minutes, cell_size=50.0, method="idw", k=8)
|
|
66
|
+
isochrones = isolines.isobands(z, [0, 5, 10, 15], transform=gt)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A rasterio dataset can be used directly:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
r = isolines.read_raster("dem.tif") # needs the "raster" extra; NaN for no-data
|
|
73
|
+
lines = isolines.contour_lines(r.z, isolines.nice_levels(r.z), transform=r.transform)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
See [`examples/quickstart.py`](examples/quickstart.py) for a DEM, a noise
|
|
77
|
+
map and an isochrone example.
|
|
78
|
+
|
|
79
|
+
## Quick start (command line)
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# contour lines every 10 m from a GeoTIFF (or an Esri ASCII .asc grid)
|
|
83
|
+
isolines lines dem.tif --interval 10 --smooth 2 -o contours.geojson
|
|
84
|
+
|
|
85
|
+
# filled bands with explicit breaks, open-ended below and above
|
|
86
|
+
isolines bands noise.asc --breaks 55,60,65,70 --open-ends -o noise.geojson
|
|
87
|
+
|
|
88
|
+
# bands straight from a point CSV (interpolated to a 50 m grid first)
|
|
89
|
+
isolines bands times.csv --value-col minutes --cell-size 50 --method idw \
|
|
90
|
+
--breaks 0,5,10,15 -o isochrones.geojson
|
|
91
|
+
|
|
92
|
+
# just the interpolation, written as an Esri ASCII grid
|
|
93
|
+
isolines grid times.csv --value-col minutes --cell-size 50 -o times.asc
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Without `--levels`/`--breaks`/`--interval`, about ten round-numbered levels
|
|
97
|
+
are chosen. `--simplify`, `--precision`, `--label-format`, `--min-length`
|
|
98
|
+
and `--min-area` are available; `isolines <command> --help` lists all
|
|
99
|
+
options.
|
|
100
|
+
|
|
101
|
+
## API overview
|
|
102
|
+
|
|
103
|
+
| Function / class | Purpose |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `contour_lines(z, levels, *, x, y, transform, nodata, pixel_is_area, smooth, simplify, min_length, label_format)` | List of `ContourLine(level, coords, closed, label)` |
|
|
106
|
+
| `isobands(z, breaks, *, ..., min_area, keep_empty)` | List of `IsoBand(lower, upper, polygons, label)`, one per interval `[lower, upper)` |
|
|
107
|
+
| `GeoTransform` / `as_geotransform` | GDAL 6-coefficient transform; accepts GDAL tuples and rasterio `Affine` |
|
|
108
|
+
| `idw`, `nearest`, `linear`, `points_to_grid`, `GridSpec` | Scattered points to grid |
|
|
109
|
+
| `interval_levels`, `nice_interval`, `nice_levels` | Level selection |
|
|
110
|
+
| `chaikin`, `douglas_peucker`, `signed_area`, `line_length`, `point_in_ring` | Geometry helpers |
|
|
111
|
+
| `assemble_polygons`, `orient_polygon`, `validate_polygon` | Polygon building and validity checking |
|
|
112
|
+
| `read_ascii_grid`, `write_ascii_grid`, `read_raster`, `load_grid`, `read_points_csv` | Input |
|
|
113
|
+
| `to_geojson`, `write_geojson`, `.to_feature()` | GeoJSON output |
|
|
114
|
+
|
|
115
|
+
Coordinates: with `transform`, grid value `z[row, col]` is placed at the
|
|
116
|
+
pixel centre (`pixel_is_area=True`, the GDAL/rasterio convention) or the
|
|
117
|
+
pixel corner (`False`). With `x`/`y` (1-D, strictly monotonic), at
|
|
118
|
+
`(x[col], y[row])`. With neither, at `(col, row)`.
|
|
119
|
+
|
|
120
|
+
GeoJSON properties: lines carry `level`, `label`, `closed`, `length`;
|
|
121
|
+
bands carry `lower`, `upper` (`null` for infinite), `label`, `area`.
|
|
122
|
+
|
|
123
|
+
## Method and sources
|
|
124
|
+
|
|
125
|
+
- **Marching squares.** Each grid cell is classified by which corners are
|
|
126
|
+
`>= level` (16 cases); crossing points are placed on cell edges by linear
|
|
127
|
+
interpolation between the two edge nodes. This is the 2-D case of
|
|
128
|
+
Lorensen, W. E. & Cline, H. E. (1987), *Marching cubes: a high resolution
|
|
129
|
+
3D surface construction algorithm*, SIGGRAPH Computer Graphics 21(4),
|
|
130
|
+
163-169; see also Maple, C. (2003), *Geometric design and space planning
|
|
131
|
+
using the marching squares and marching cube algorithms*, Proc.
|
|
132
|
+
GMAG 2003, 90-95.
|
|
133
|
+
- **Saddle disambiguation.** In the two ambiguous cases (diagonal corners
|
|
134
|
+
above), the mean of the four corner values stands in for the value at the
|
|
135
|
+
cell centre: when it is `>= level` the high corners are joined through the
|
|
136
|
+
cell, otherwise the low corners are. (The asymptotic decider of Nielson,
|
|
137
|
+
G. M. & Hamann, B. (1991), *The asymptotic decider*, Proc. IEEE
|
|
138
|
+
Visualization '91, 83-91, is the bilinear-exact alternative; it is not
|
|
139
|
+
used here.)
|
|
140
|
+
- **Joining and orientation.** Every segment is directed so that the
|
|
141
|
+
`>= level` side lies on its left. Each edge crossing then starts exactly
|
|
142
|
+
one segment and ends exactly one, so joining is unambiguous and closed
|
|
143
|
+
rings are detected exactly by shared crossing identities, not by
|
|
144
|
+
coordinate tolerance.
|
|
145
|
+
- **Isobands.** The boundary of the band `lower <= z < upper` is made of
|
|
146
|
+
the oriented contour segments of `lower`, the reversed contour segments of
|
|
147
|
+
`upper`, and the parts of the valid-domain boundary (grid edge and no-data
|
|
148
|
+
cells) whose values fall in the band. These are traced into closed rings;
|
|
149
|
+
counter-clockwise rings are exteriors, clockwise rings are holes and go to
|
|
150
|
+
the smallest exterior containing them. Rings that touch themselves where
|
|
151
|
+
the no-data domain pinches are split, so polygons are valid under the OGC
|
|
152
|
+
Simple Features rules. Output rings follow **RFC 7946, section 3.1.6**
|
|
153
|
+
(exterior counter-clockwise, holes clockwise; first and last positions
|
|
154
|
+
equal).
|
|
155
|
+
- **Exact level values.** A node equal to a level counts as above it.
|
|
156
|
+
Crossing parameters are clamped to `[1e-7, 1 - 1e-7]` of the edge, so a
|
|
157
|
+
contour through a node never produces coincident points on two edges;
|
|
158
|
+
the microscopic rings this would leave around an isolated node equal to
|
|
159
|
+
the level (for example a peak equal to the top level) are dropped.
|
|
160
|
+
- **Smoothing:** Chaikin, G. M. (1974), *An algorithm for high-speed curve
|
|
161
|
+
generation*, Computer Graphics and Image Processing 3(4), 346-349.
|
|
162
|
+
- **Simplification:** Douglas, D. H. & Peucker, T. K. (1973), *Algorithms
|
|
163
|
+
for the reduction of the number of points required to represent a
|
|
164
|
+
digitized line or its caricature*, Cartographica 10(2), 112-122.
|
|
165
|
+
- **IDW:** Shepard, D. (1968), *A two-dimensional interpolation function
|
|
166
|
+
for irregularly-spaced data*, Proc. 23rd ACM National Conference,
|
|
167
|
+
517-524. Weights `d ** -power`; a query on a sample takes its value.
|
|
168
|
+
- **Linear TIN:** Delaunay triangulation and barycentric interpolation via
|
|
169
|
+
`scipy.interpolate.LinearNDInterpolator`; NaN outside the convex hull.
|
|
170
|
+
- **Geotransform:** the GDAL six-coefficient affine model (GDAL Raster Data
|
|
171
|
+
Model documentation). **Esri ASCII grid:** header keys and row order as in
|
|
172
|
+
the Esri "Esri ASCII raster format" documentation.
|
|
173
|
+
|
|
174
|
+
The test suite checks analytic surfaces with known answers: a plane
|
|
175
|
+
(contour position and band area exact), a cone (contours within the
|
|
176
|
+
`1/(8r)` interpolation error of a circle, annulus area), the saddle `z = x*y`
|
|
177
|
+
(contour on the axes, saddle-cell areas computed by hand), nested
|
|
178
|
+
peak/pit/plateau surfaces (island in a hole), no-data holes and pinches,
|
|
179
|
+
plus 40 random integer grids whose values sit exactly on the breaks. Every
|
|
180
|
+
band polygon is validated, and the bands are checked to tile the valid
|
|
181
|
+
domain; shapely (`is_valid`, areas) is used as an independent check when
|
|
182
|
+
installed.
|
|
183
|
+
|
|
184
|
+
## Limitations
|
|
185
|
+
|
|
186
|
+
- Linear interpolation along edges means contours of curved surfaces are
|
|
187
|
+
chords; refine the grid for more accuracy.
|
|
188
|
+
- The saddle rule uses the cell mean, not the bilinear asymptotic decider;
|
|
189
|
+
in rare saddle cells the topology can differ from bilinear interpolation.
|
|
190
|
+
- `smooth` and `simplify` work ring by ring and are not topology-preserving:
|
|
191
|
+
neighbouring bands may slightly overlap or leave slivers, and a smoothed
|
|
192
|
+
line can cross a neighbouring level. Leave them off when exact tiling
|
|
193
|
+
matters.
|
|
194
|
+
- Rounding coordinates (`precision`) can merge points that are only `1e-7`
|
|
195
|
+
cells apart at exact-level nodes; keep enough decimals for your units.
|
|
196
|
+
- `idw` and `nearest` are brute force (chunked numpy), fine for tens of
|
|
197
|
+
thousands of samples; IDW is an exact interpolator but not a statistical
|
|
198
|
+
one (no kriging, no barriers). A noise or travel-time surface interpolated
|
|
199
|
+
from points is only as good as the sample coverage.
|
|
200
|
+
- GeoJSON output carries no CRS (RFC 7946 assumes WGS 84); projected
|
|
201
|
+
coordinates are written as given.
|
|
202
|
+
- Lines and polygons are built in memory; very large grids (10^8 cells)
|
|
203
|
+
need tiling.
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
python -m pip install -e ".[dev,scipy,raster]"
|
|
209
|
+
python -m pytest tests
|
|
210
|
+
python -m ruff check . && python -m ruff format --check src tests
|
|
211
|
+
python -m mypy src
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Academic citation
|
|
215
|
+
|
|
216
|
+
```bibtex
|
|
217
|
+
@software{eminoglu_isolines,
|
|
218
|
+
author = {Eminoğlu, Yusuf},
|
|
219
|
+
title = {isolines},
|
|
220
|
+
year = {2026},
|
|
221
|
+
version = {0.1.0},
|
|
222
|
+
url = {https://gitlab.com/geophilo1/isolines_sdk}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT License. Copyright (c) 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""isolines quick start: DEM contours, a noise map and a travel-time isochrone.
|
|
2
|
+
|
|
3
|
+
Run from the repository root:
|
|
4
|
+
|
|
5
|
+
python examples/quickstart.py
|
|
6
|
+
|
|
7
|
+
Writes three GeoJSON files next to this script (coordinates in a projected,
|
|
8
|
+
metre-based CRS; tell your GIS which one, RFC 7946 assumes WGS 84).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
import isolines
|
|
19
|
+
|
|
20
|
+
HERE = Path(__file__).parent
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def dem_contours() -> None:
|
|
24
|
+
# A synthetic 25 m DEM: two hills on a gentle slope, with a no-data lake.
|
|
25
|
+
yy, xx = np.mgrid[0:120, 0:160].astype(float)
|
|
26
|
+
z = (
|
|
27
|
+
40
|
|
28
|
+
+ 0.15 * xx
|
|
29
|
+
+ 60 * np.exp(-((xx - 50) ** 2 + (yy - 60) ** 2) / 600)
|
|
30
|
+
+ 35 * np.exp(-((xx - 115) ** 2 + (yy - 40) ** 2) / 300)
|
|
31
|
+
)
|
|
32
|
+
z[90:105, 120:140] = np.nan
|
|
33
|
+
gt = isolines.GeoTransform.north_up(500000.0, 4200000.0, 25.0)
|
|
34
|
+
|
|
35
|
+
levels = isolines.interval_levels(float(np.nanmin(z)), float(np.nanmax(z)), 10)
|
|
36
|
+
lines = isolines.contour_lines(
|
|
37
|
+
z, levels, transform=gt, smooth=2, simplify=2.0, label_format="{:g} m"
|
|
38
|
+
)
|
|
39
|
+
isolines.write_geojson(HERE / "dem_contours.geojson", lines, precision=2)
|
|
40
|
+
print(f"DEM: {len(lines)} contour lines at {levels[0]:g}..{levels[-1]:g} m")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def noise_map() -> None:
|
|
44
|
+
# Sound level (dB) at scattered receivers near a road along y = 0,
|
|
45
|
+
# free-field line-source decay of 10*log10(r) dB (illustrative only).
|
|
46
|
+
rng = np.random.default_rng(42)
|
|
47
|
+
x = rng.uniform(-500, 500, 300)
|
|
48
|
+
y = rng.uniform(5, 400, 300)
|
|
49
|
+
db = 75 - 10 * np.log10(y / 5)
|
|
50
|
+
z, gt = isolines.points_to_grid(x, y, db, 10.0, method="idw", k=12)
|
|
51
|
+
bands = isolines.isobands(z, [-math.inf, 55, 60, 65, 70, math.inf], transform=gt)
|
|
52
|
+
isolines.write_geojson(HERE / "noise_bands.geojson", bands, precision=1)
|
|
53
|
+
for band in bands:
|
|
54
|
+
print(f"noise {band.label:>8} dB: {band.area / 1e4:8.1f} ha")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def isochrones() -> None:
|
|
58
|
+
# Walking time (minutes) sampled at network nodes around a station.
|
|
59
|
+
rng = np.random.default_rng(7)
|
|
60
|
+
x = rng.uniform(-2000, 2000, 500)
|
|
61
|
+
y = rng.uniform(-2000, 2000, 500)
|
|
62
|
+
detour = 1.25 # network distance / straight-line distance
|
|
63
|
+
minutes = np.hypot(x, y) * detour / 80.0 # 80 m per minute
|
|
64
|
+
z, gt = isolines.points_to_grid(x, y, minutes, 50.0, method="nearest")
|
|
65
|
+
bands = isolines.isobands(z, [0, 5, 10, 15], transform=gt, smooth=1)
|
|
66
|
+
isolines.write_geojson(HERE / "isochrones.geojson", bands, precision=1)
|
|
67
|
+
for band in bands:
|
|
68
|
+
print(f"isochrone {band.label:>6} min: {band.area / 1e6:6.2f} km2")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
dem_contours()
|
|
73
|
+
noise_map()
|
|
74
|
+
isochrones()
|