geofix 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.
- geofix-0.1.0/.pre-commit-hooks.yaml +17 -0
- geofix-0.1.0/CHANGELOG.md +31 -0
- geofix-0.1.0/CITATION.cff +19 -0
- geofix-0.1.0/LICENSE +21 -0
- geofix-0.1.0/MANIFEST.in +10 -0
- geofix-0.1.0/PKG-INFO +382 -0
- geofix-0.1.0/README.md +342 -0
- geofix-0.1.0/examples/bad.geojson +8 -0
- geofix-0.1.0/examples/good.geojson +81 -0
- geofix-0.1.0/examples/points.geojsonl +3 -0
- geofix-0.1.0/pyproject.toml +131 -0
- geofix-0.1.0/setup.cfg +4 -0
- geofix-0.1.0/src/geofix/__init__.py +59 -0
- geofix-0.1.0/src/geofix/__main__.py +7 -0
- geofix-0.1.0/src/geofix/api.py +359 -0
- geofix-0.1.0/src/geofix/cli.py +350 -0
- geofix-0.1.0/src/geofix/config.py +102 -0
- geofix-0.1.0/src/geofix/extent.py +121 -0
- geofix-0.1.0/src/geofix/geometry.py +649 -0
- geofix-0.1.0/src/geofix/model.py +90 -0
- geofix-0.1.0/src/geofix/options.py +40 -0
- geofix-0.1.0/src/geofix/py.typed +0 -0
- geofix-0.1.0/src/geofix/reader.py +258 -0
- geofix-0.1.0/src/geofix/repair.py +681 -0
- geofix-0.1.0/src/geofix/report.py +180 -0
- geofix-0.1.0/src/geofix/rules.py +349 -0
- geofix-0.1.0/src/geofix/stats.py +244 -0
- geofix-0.1.0/src/geofix/validate.py +903 -0
- geofix-0.1.0/src/geofix.egg-info/PKG-INFO +382 -0
- geofix-0.1.0/src/geofix.egg-info/SOURCES.txt +41 -0
- geofix-0.1.0/src/geofix.egg-info/dependency_links.txt +1 -0
- geofix-0.1.0/src/geofix.egg-info/entry_points.txt +2 -0
- geofix-0.1.0/src/geofix.egg-info/requires.txt +12 -0
- geofix-0.1.0/src/geofix.egg-info/top_level.txt +1 -0
- geofix-0.1.0/tests/conftest.py +56 -0
- geofix-0.1.0/tests/test_api_files.py +243 -0
- geofix-0.1.0/tests/test_cli.py +238 -0
- geofix-0.1.0/tests/test_geometry_kernel.py +183 -0
- geofix-0.1.0/tests/test_performance.py +114 -0
- geofix-0.1.0/tests/test_reader.py +143 -0
- geofix-0.1.0/tests/test_repair.py +292 -0
- geofix-0.1.0/tests/test_rules_geometry.py +378 -0
- geofix-0.1.0/tests/test_rules_structure.py +279 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
- id: geofix
|
|
2
|
+
name: geofix (validate GeoJSON)
|
|
3
|
+
description: Validate GeoJSON / GeoJSONSeq files against RFC 7946.
|
|
4
|
+
entry: geofix check
|
|
5
|
+
language: python
|
|
6
|
+
files: '\.(geojson|geojsonl|geojsons|geojsonseq|ndgeojson)$'
|
|
7
|
+
types: [file]
|
|
8
|
+
require_serial: false
|
|
9
|
+
|
|
10
|
+
- id: geofix-fix
|
|
11
|
+
name: geofix (repair GeoJSON)
|
|
12
|
+
description: Repair GeoJSON files in place (winding, closing rings, duplicates, bbox, ...).
|
|
13
|
+
entry: geofix fix --in-place
|
|
14
|
+
language: python
|
|
15
|
+
files: '\.(geojson|geojsonl|geojsons|geojsonseq|ndgeojson)$'
|
|
16
|
+
types: [file]
|
|
17
|
+
require_serial: false
|
|
@@ -0,0 +1,31 @@
|
|
|
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-08
|
|
8
|
+
### Added
|
|
9
|
+
- **Validator** for RFC 7946 GeoJSON with 38 rules (GF000-GF302): document structure,
|
|
10
|
+
member types, ids, foreign/reserved members and `crs`; position arity, NaN/Infinity,
|
|
11
|
+
longitude/latitude ranges, swapped axes, projected-coordinate detection with EPSG:3857
|
|
12
|
+
back-projection and UTM/TM hints, excessive precision, duplicate vertices, mixed
|
|
13
|
+
dimensions; empty/null geometries, short lines, unclosed/short/zero-area rings, winding
|
|
14
|
+
order (right-hand rule), antimeridian jumps; `bbox` validity, containment and tightness.
|
|
15
|
+
Every issue has a code, severity, JSON path, line, message and fix hint.
|
|
16
|
+
- **Pure-Python topology checks**: ring self-intersections (crossings, touches, spikes),
|
|
17
|
+
holes outside or crossing their shell, crossing/overlapping/nested holes. Uses a convexity
|
|
18
|
+
shortcut, monotone-chain bisection and a uniform grid; no shapely or GEOS required.
|
|
19
|
+
- **Repairer** (`geofix fix`, `geofix.fix`, `geofix.fix_file`): rounding, duplicate removal,
|
|
20
|
+
ring closing, degenerate hole/part removal, bow-tie splitting in pure Python,
|
|
21
|
+
`shapely.make_valid` for the rest (optional `shapely` extra), rewinding, bbox
|
|
22
|
+
recomputation, removal of empty features and of WGS 84 `crs` members, missing
|
|
23
|
+
`properties`/`geometry` members. Every modification is reported as a `Change`; repairs are
|
|
24
|
+
idempotent and valid files are left byte-for-byte untouched.
|
|
25
|
+
- **Streaming reader** for FeatureCollections (feature by feature) and newline-delimited
|
|
26
|
+
GeoJSON / GeoJSONSeq (RFC 8142 record separators), with line numbers and recovery from bad
|
|
27
|
+
records.
|
|
28
|
+
- **CLI** `geofix check|fix|stats|rules` with exit codes for CI, `--format text|json|github`
|
|
29
|
+
(GitHub Actions annotations), `--select`/`--ignore`, `--fail-on`, `--precision`,
|
|
30
|
+
directory expansion, and `[tool.geofix]` configuration in `pyproject.toml`.
|
|
31
|
+
- `.pre-commit-hooks.yaml` with `geofix` (check) and `geofix-fix` (repair in place) hooks.
|
|
@@ -0,0 +1,19 @@
|
|
|
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: "yusufeminoglu@gmail.com"
|
|
7
|
+
orcid: "https://orcid.org/0009-0005-6000-2934"
|
|
8
|
+
title: "geofix"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-08"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://github.com/YusufEminoglu/geofix_sdk"
|
|
13
|
+
url: "https://pypi.org/project/geofix/"
|
|
14
|
+
abstract: "Fast, dependency-light GeoJSON (RFC 7946) validator and repairer for local runs, pre-commit hooks and CI."
|
|
15
|
+
keywords:
|
|
16
|
+
- geojson
|
|
17
|
+
- rfc7946
|
|
18
|
+
- validation
|
|
19
|
+
- gis
|
geofix-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.
|
geofix-0.1.0/MANIFEST.in
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include CITATION.cff
|
|
5
|
+
include pyproject.toml
|
|
6
|
+
include .pre-commit-hooks.yaml
|
|
7
|
+
include src/geofix/py.typed
|
|
8
|
+
recursive-include src/geofix *.py
|
|
9
|
+
recursive-include tests *.py
|
|
10
|
+
recursive-include examples *.geojson *.geojsonl
|
geofix-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geofix
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fast, dependency-light GeoJSON (RFC 7946) validator and repairer for local runs, pre-commit and CI.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YusufEminoglu/geofix_sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/YusufEminoglu/geofix_sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/YusufEminoglu/geofix_sdk/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/YusufEminoglu/geofix_sdk#readme
|
|
11
|
+
Project-URL: Changelog, https://github.com/YusufEminoglu/geofix_sdk/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: geojson,rfc7946,validation,validator,linter,repair,make-valid,gis,geospatial,topology,self-intersection,winding-order,pre-commit,github-actions,ci,geojsonseq,ndjson
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
22
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing
|
|
24
|
+
Classifier: Environment :: Console
|
|
25
|
+
Classifier: Intended Audience :: Science/Research
|
|
26
|
+
Classifier: Intended Audience :: Developers
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: tomli>=1.1.0; python_version < "3.11"
|
|
32
|
+
Provides-Extra: shapely
|
|
33
|
+
Requires-Dist: shapely>=2.0.0; extra == "shapely"
|
|
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
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# geofix
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/geofix/)
|
|
44
|
+
[](https://pypi.org/project/geofix/)
|
|
45
|
+
[](https://github.com/YusufEminoglu/geofix_sdk/actions/workflows/ci.yml)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
|
|
48
|
+
**Fast, dependency-light GeoJSON validator and repairer** for the command line, pre-commit
|
|
49
|
+
hooks and CI. It checks files against [RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946)
|
|
50
|
+
(structure, coordinates, winding, bounding boxes) and polygon topology (self-intersections,
|
|
51
|
+
holes), tells you *where* each problem is, and can repair most of them, reporting every change
|
|
52
|
+
it makes.
|
|
53
|
+
|
|
54
|
+
- **Pure Python, no required dependencies** (only `tomli` on Python < 3.11 for reading
|
|
55
|
+
`pyproject.toml`). Topology checks run without shapely or GEOS.
|
|
56
|
+
- **Streams large files**: a `FeatureCollection` is read one feature at a time, and
|
|
57
|
+
newline-delimited GeoJSON (GeoJSONSeq / RFC 8142, `.geojsonl`, `.ndgeojson`) one record at a
|
|
58
|
+
time.
|
|
59
|
+
- **Made for CI**: exit codes, `--format github` annotations, `--format json`,
|
|
60
|
+
`--select`/`--ignore` rule codes, `[tool.geofix]` configuration, a pre-commit hook.
|
|
61
|
+
- **Explains CRS mistakes**: "this looks like EPSG:3857 Web Mercator (the centre would be
|
|
62
|
+
lon 28.99762, lat 40.97734)", UTM/TM eastings, swapped `[lat, lon]` axes.
|
|
63
|
+
- **Repairs safely**: `geofix fix` never changes data without a recorded change, leaves valid
|
|
64
|
+
files byte-for-byte untouched, and is idempotent.
|
|
65
|
+
|
|
66
|
+
```console
|
|
67
|
+
pip install geofix # pure Python
|
|
68
|
+
pip install "geofix[shapely]" # + shapely.make_valid for complex topology repairs
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Command line
|
|
72
|
+
|
|
73
|
+
### `geofix check`
|
|
74
|
+
|
|
75
|
+
```console
|
|
76
|
+
$ geofix check examples/bad.geojson
|
|
77
|
+
examples/bad.geojson:2:1: GF212 warning: exterior ring is clockwise [$.features[0].geometry.coordinates[0]]
|
|
78
|
+
hint: Reverse the ring (ogr2ogr -lco RFC7946=YES writes the right order).
|
|
79
|
+
examples/bad.geojson:3:1: GF220 error: ring crosses itself at (29.025, 41.005) [$.features[1].geometry.coordinates[0]]
|
|
80
|
+
hint: Split bow-ties into separate polygons or rebuild with shapely.make_valid.
|
|
81
|
+
examples/bad.geojson:4:1: GF109 warning: 1 consecutive duplicate position(s), first [29.05, 41] [$.features[2].geometry.coordinates[0][2]]
|
|
82
|
+
hint: Remove the repeated positions.
|
|
83
|
+
examples/bad.geojson:4:1: GF210 error: ring is not closed: first position [29.04, 41], last [29.04, 41.01] [$.features[2].geometry.coordinates[0]]
|
|
84
|
+
hint: Repeat the first position at the end of the ring.
|
|
85
|
+
examples/bad.geojson:4:1: GF108 info: 1 of 5 position(s) have more than 7 decimals [$.features[2].geometry.coordinates[0][3]]
|
|
86
|
+
hint: Round coordinates; 6 decimals is ~10 cm, 7 is ~1 cm (RFC 7946 section 11.2).
|
|
87
|
+
examples/bad.geojson:5:1: GF006 warning: id 'parcel-3' is also used by $.features[2] [$.features[3].id]
|
|
88
|
+
hint: Give every feature its own id, or drop ids that are not identifiers.
|
|
89
|
+
examples/bad.geojson:5:1: GF107 error: coordinates (x 3.228e+06..3.228e+06, y 5.009e+06..5.009e+06) look projected, not WGS 84 longitude/latitude: this looks like EPSG:3857 Web Mercator (the centre would be lon 28.99762, lat 40.97734) or a Gauss-Kruger/TM grid with zone-prefixed eastings [$.features[3].geometry.coordinates]
|
|
90
|
+
hint: Reproject to EPSG:4326, e.g. ogr2ogr -t_srs EPSG:4326 -lco RFC7946=YES out.geojson in.geojson.
|
|
91
|
+
examples/bad.geojson:6:1: GF106 warning: latitude 129.0 is out of range while every longitude is within +/-90: positions look like [lat, lon] [$.features[4].geometry.coordinates[0]]
|
|
92
|
+
hint: GeoJSON order is longitude first; swap x and y (EPSG:4326 axis order is a common cause).
|
|
93
|
+
examples/bad.geojson:7:1: GF200 warning: MultiPolygon has empty coordinates [$.features[5].geometry]
|
|
94
|
+
hint: Remove the feature, or use "geometry": null for features without a location.
|
|
95
|
+
examples/bad.geojson:1:1: GF301 error: "bbox" [29, 41, 29.03, 41.01] does not contain the coordinates (extent [29, 41, 3228000.5, 5009000.25]) [$.bbox]
|
|
96
|
+
hint: Recompute the bbox from the coordinates.
|
|
97
|
+
|
|
98
|
+
Found 10 issues (4 errors, 5 warnings, 1 info) in 1 of 1 file.
|
|
99
|
+
$ echo $?
|
|
100
|
+
1
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Each line is `file:line:column: CODE severity: message [JSON path]`. The line is where the
|
|
104
|
+
feature (or record) starts; the JSON path points inside it. Directories are searched
|
|
105
|
+
recursively for `.geojson`, `.geojsonl`, `.geojsons`, `.geojsonseq`, `.ndgeojson` and
|
|
106
|
+
`.geo.json` files; `-` reads standard input.
|
|
107
|
+
|
|
108
|
+
Useful options: `--select GF2,GF3`, `--ignore GF007,GF108` (codes or prefixes, `ALL`),
|
|
109
|
+
`--fail-on error|warning|info`, `--precision N`, `--statistics`, `--no-hints`,
|
|
110
|
+
`--format text|json|github`.
|
|
111
|
+
|
|
112
|
+
### `geofix fix`
|
|
113
|
+
|
|
114
|
+
```console
|
|
115
|
+
$ geofix fix examples/bad.geojson -o fixed.geojson --no-hints
|
|
116
|
+
examples/bad.geojson: fixed 9 problems (GF108 x1, GF109 x1, GF200 x1, GF210 x1, GF212 x2, GF220 x2, GF301 x1)
|
|
117
|
+
examples/bad.geojson:5:1: GF006 warning: id 'parcel-3' is also used by $.features[2] [$.features[3].id]
|
|
118
|
+
examples/bad.geojson:5:1: GF107 error: coordinates (x 3.228e+06..3.228e+06, y 5.009e+06..5.009e+06) look projected, not WGS 84 longitude/latitude: this looks like EPSG:3857 Web Mercator (the centre would be lon 28.99762, lat 40.97734) or a Gauss-Kruger/TM grid with zone-prefixed eastings [$.features[3].geometry.coordinates]
|
|
119
|
+
examples/bad.geojson:6:1: GF106 warning: latitude 129.0 is out of range while every longitude is within +/-90: positions look like [lat, lon] [$.features[4].geometry.coordinates[0]]
|
|
120
|
+
|
|
121
|
+
Applied 9 fixes to 1 file. Found 3 remaining issues (1 error, 2 warnings) in 1 of 1 file.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
What it did: rewound the clockwise shell, split the bow-tie into a two-part MultiPolygon,
|
|
125
|
+
removed the duplicate vertex, closed the open ring, rounded the over-precise coordinate,
|
|
126
|
+
removed the feature with an empty geometry and recomputed the collection `bbox`. It did
|
|
127
|
+
**not** guess a CRS or swap axes: those are reported for a human to decide.
|
|
128
|
+
|
|
129
|
+
- `geofix fix FILE` writes the result to standard output (the report goes to stderr);
|
|
130
|
+
`-o OUT` writes a file; `-i/--in-place` rewrites inputs **only when something changed**;
|
|
131
|
+
`--dry-run` reports what would change and exits 1 if anything would.
|
|
132
|
+
- `--show-changes` lists every change instead of a per-file summary;
|
|
133
|
+
`--exit-nonzero-on-fix` exits 1 when a file was modified; `--no-shapely` keeps repairs pure
|
|
134
|
+
Python.
|
|
135
|
+
- Collections are written one feature per line (readable diffs); `--indent N` pretty-prints
|
|
136
|
+
single objects. Collection members keep their order, except `bbox`, which is written after
|
|
137
|
+
`features` because it can only be computed once every feature has been repaired.
|
|
138
|
+
- Repairs run in this order: round (GF108), drop duplicates (GF109), close rings (GF210),
|
|
139
|
+
drop degenerate holes/parts (GF211/GF214), topology (GF220-GF222), rewind (GF212/GF213),
|
|
140
|
+
recompute bboxes (GF300-GF302). A second `geofix fix` on the output changes nothing.
|
|
141
|
+
- Rounding to the configured precision (7 decimals by default, ~1 cm) is a repair like any
|
|
142
|
+
other: disable it with `--ignore GF108` or change it with `--precision 9`.
|
|
143
|
+
|
|
144
|
+
### `geofix stats` and `geofix rules`
|
|
145
|
+
|
|
146
|
+
```console
|
|
147
|
+
$ geofix stats examples/bad.geojson
|
|
148
|
+
examples/bad.geojson
|
|
149
|
+
kind: FeatureCollection
|
|
150
|
+
size: 1.2 kB
|
|
151
|
+
features: 6
|
|
152
|
+
geometry types: Polygon 3, Point 1, LineString 1, MultiPolygon 1
|
|
153
|
+
null / empty: 0 / 1
|
|
154
|
+
positions: 18 (0 with z)
|
|
155
|
+
max decimals: 10
|
|
156
|
+
bbox: [29, 41, 3.228e+06, 5.009e+06]
|
|
157
|
+
ids: 6 (5 unique)
|
|
158
|
+
properties: name (5)
|
|
159
|
+
read in: 0.00 s
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`geofix rules` lists every rule (`--format text|json|markdown`).
|
|
163
|
+
|
|
164
|
+
### Exit codes
|
|
165
|
+
|
|
166
|
+
| Status | Meaning |
|
|
167
|
+
|---|---|
|
|
168
|
+
| 0 | No issue at or above `--fail-on` (default `error`) |
|
|
169
|
+
| 1 | Issues at or above `--fail-on`; also `fix --dry-run` / `--exit-nonzero-on-fix` when a fix is (or was) needed |
|
|
170
|
+
| 2 | Usage or configuration error, or an input that cannot be read |
|
|
171
|
+
|
|
172
|
+
Warnings do not fail a run by default; use `--fail-on warning` (or `fail-on = "warning"` in
|
|
173
|
+
the configuration) for strict RFC 7946 conformance, e.g. ring winding.
|
|
174
|
+
|
|
175
|
+
## Rules
|
|
176
|
+
|
|
177
|
+
| Code | Name | Severity | Checks | `geofix fix` |
|
|
178
|
+
|---|---|---|---|---|
|
|
179
|
+
| GF000 | invalid-json | error | The input is not valid UTF-8 JSON. | report only |
|
|
180
|
+
| GF001 | invalid-type | error | Missing or unknown "type" member. | report only |
|
|
181
|
+
| GF002 | missing-member | error | A required member is missing (Feature "geometry"/"properties", FeatureCollection "features", geometry "coordinates", GeometryCollection "geometries"). | Adds "properties": {} or "geometry": null to features that lack them. |
|
|
182
|
+
| GF003 | invalid-member | error | A member has the wrong JSON type (e.g. "features" is not an array, a feature is not a Feature object, "geometry" is not an object or null). | report only |
|
|
183
|
+
| GF004 | invalid-properties | error | "properties" must be a JSON object or null. | report only |
|
|
184
|
+
| GF005 | invalid-id | error | Feature "id" must be a string or a number. | report only |
|
|
185
|
+
| GF006 | duplicate-id | warning | Feature "id" is not unique within the collection. | report only |
|
|
186
|
+
| GF007 | foreign-member | info | Member that RFC 7946 does not define for this object (a foreign member). | report only |
|
|
187
|
+
| GF008 | reserved-member | error | Member that changes GeoJSON semantics on this object type (RFC 7946 section 7.1), e.g. "coordinates" on a Feature or "properties" on a geometry. | report only |
|
|
188
|
+
| GF009 | crs-member | warning | "crs" member present; RFC 7946 removed it and requires WGS 84 longitude/latitude. | Removes "crs" when it already names WGS 84 (CRS84 / EPSG:4326). |
|
|
189
|
+
| GF010 | nested-geometry-collection | warning | GeometryCollection nested inside another GeometryCollection (RFC 7946 3.1.8 says SHOULD avoid). | report only |
|
|
190
|
+
| GF100 | invalid-coordinates | error | "coordinates" has the wrong nesting depth or holds non-numeric values. | report only |
|
|
191
|
+
| GF101 | position-too-short | error | Position has fewer than two numbers. | report only |
|
|
192
|
+
| GF102 | position-too-long | warning | Position has more than three numbers (RFC 7946 3.1.1: SHOULD NOT). | report only |
|
|
193
|
+
| GF103 | non-finite-coordinate | error | Coordinate is NaN or Infinity (non-standard JSON literals, or overflowing numbers). | report only |
|
|
194
|
+
| GF104 | longitude-out-of-range | error | Longitude outside [-180, 180]. | report only |
|
|
195
|
+
| GF105 | latitude-out-of-range | error | Latitude outside [-90, 90]. | report only |
|
|
196
|
+
| GF106 | swapped-axes | warning | Coordinates look like [lat, lon] instead of [lon, lat]. | report only |
|
|
197
|
+
| GF107 | projected-coordinates | error | Coordinates look projected (metres: EPSG:3857, UTM, national TM grids), not WGS 84. | report only |
|
|
198
|
+
| GF108 | excessive-precision | info | Coordinates carry more decimals than the configured precision (default 7, ~1 cm). | Rounds longitude/latitude to the configured precision. |
|
|
199
|
+
| GF109 | duplicate-vertex | warning | Consecutive duplicate positions in a line or ring. | Drops consecutive duplicate positions. |
|
|
200
|
+
| GF110 | mixed-dimensions | info | Geometry mixes 2D and 3D positions. | report only |
|
|
201
|
+
| GF200 | empty-geometry | warning | Geometry has no coordinates (or a GeometryCollection has no geometries). | Removes features whose geometry is empty from collections and sequences. |
|
|
202
|
+
| GF201 | null-geometry | info | Feature has "geometry": null (allowed, but many tools skip such features). | report only |
|
|
203
|
+
| GF202 | line-too-short | error | LineString has fewer than two positions. | report only |
|
|
204
|
+
| GF203 | zero-length-line | warning | LineString whose positions are all identical. | report only |
|
|
205
|
+
| GF210 | unclosed-ring | error | Linear ring's first and last positions differ. | Appends the first position to close the ring. |
|
|
206
|
+
| GF211 | ring-too-short | error | Linear ring has fewer than four positions. | Drops degenerate holes and polygon parts (always reported). |
|
|
207
|
+
| GF212 | exterior-clockwise | warning | Exterior ring is clockwise; RFC 7946 3.1.6 requires counter-clockwise. | Reverses the ring. |
|
|
208
|
+
| GF213 | hole-counterclockwise | warning | Interior ring (hole) is counter-clockwise; RFC 7946 3.1.6 requires clockwise. | Reverses the ring. |
|
|
209
|
+
| GF214 | zero-area-ring | error | Linear ring encloses no area (all positions collinear or collapsed). | Drops degenerate holes and polygon parts (always reported). |
|
|
210
|
+
| GF220 | self-intersection | error | Ring crosses or touches itself. | Splits single-crossing (bow-tie) rings in pure Python; other cases use shapely.make_valid when shapely is installed. |
|
|
211
|
+
| GF221 | hole-outside-shell | error | Hole is not inside its exterior ring (lies outside it or crosses it). | Rebuilt with shapely.make_valid when shapely is installed. |
|
|
212
|
+
| GF222 | holes-overlap | error | Two holes of the same polygon cross, overlap or nest. | Rebuilt with shapely.make_valid when shapely is installed. |
|
|
213
|
+
| GF230 | antimeridian-crossing | warning | Consecutive positions jump more than 180 degrees of longitude; the segment probably crosses the antimeridian. | report only |
|
|
214
|
+
| GF300 | invalid-bbox | error | "bbox" is not an array of 2*n numbers with min <= max (west > east is allowed across the antimeridian). | Recomputes the bbox from the coordinates. |
|
|
215
|
+
| GF301 | bbox-mismatch | error | "bbox" does not contain all coordinates of the object. | Recomputes the bbox from the coordinates. |
|
|
216
|
+
| GF302 | bbox-not-tight | info | "bbox" is larger than the extent of the coordinates. | Recomputes the bbox from the coordinates. |
|
|
217
|
+
|
|
218
|
+
Notes on the checks:
|
|
219
|
+
|
|
220
|
+
- **Topology** is planar, in the coordinate space of the file, like the OGC simple feature
|
|
221
|
+
rules: within a ring any contact between non-adjacent edges is an error (crossing,
|
|
222
|
+
touching, or a collinear spike); between rings, crossings and shared edges are errors,
|
|
223
|
+
while a single touching point is allowed. Overlap between the *parts* of a MultiPolygon is
|
|
224
|
+
not checked.
|
|
225
|
+
- **Antimeridian**: RFC 7946 3.1.9 asks for geometries crossing 180 degrees to be cut into
|
|
226
|
+
multi-part geometries. GF230 flags segments that jump more than 180 degrees of longitude;
|
|
227
|
+
geofix does not cut them (use `ogr2ogr -wrapdateline` or the `antimeridian` package). A
|
|
228
|
+
`bbox` with west > east is accepted as an antimeridian-crossing box (RFC 7946 5.2).
|
|
229
|
+
- **CRS heuristics** (GF106/GF107) look at each geometry's extent. Values beyond +/-1000 are
|
|
230
|
+
treated as metres; eastings of 100 000-900 000 m suggest UTM or a national TM grid (for
|
|
231
|
+
example TUREF/TM), values within the Web Mercator square are back-projected so you can see
|
|
232
|
+
where the data would land if it were EPSG:3857. `[lat, lon]` is only detectable when a
|
|
233
|
+
latitude falls outside +/-90 while every longitude is within +/-90.
|
|
234
|
+
- **Precision** (GF108) counts decimals of the shortest representation of each longitude and
|
|
235
|
+
latitude (elevations are not checked).
|
|
236
|
+
|
|
237
|
+
## Configuration
|
|
238
|
+
|
|
239
|
+
`geofix` reads `[tool.geofix]` from the nearest `pyproject.toml` (or `--config FILE`;
|
|
240
|
+
`--isolated` ignores it). Command-line options override it.
|
|
241
|
+
|
|
242
|
+
```toml
|
|
243
|
+
[tool.geofix]
|
|
244
|
+
select = ["ALL"] # codes or prefixes, e.g. ["GF0", "GF2"]
|
|
245
|
+
ignore = ["GF007"] # foreign members are fine for us
|
|
246
|
+
fail-on = "warning" # error | warning | info
|
|
247
|
+
precision = 6 # GF108 threshold and fix rounding
|
|
248
|
+
shapely = true # let `geofix fix` use shapely.make_valid when installed
|
|
249
|
+
format = "text" # text | json | github
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## pre-commit
|
|
253
|
+
|
|
254
|
+
```yaml
|
|
255
|
+
# .pre-commit-config.yaml
|
|
256
|
+
repos:
|
|
257
|
+
- repo: https://github.com/YusufEminoglu/geofix_sdk
|
|
258
|
+
rev: v0.1.0
|
|
259
|
+
hooks:
|
|
260
|
+
- id: geofix # validate
|
|
261
|
+
# - id: geofix-fix # or repair in place (the commit stops if a file changed)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Both hooks run on `.geojson`, `.geojsonl`, `.geojsons`, `.geojsonseq` and `.ndgeojson` files
|
|
265
|
+
and honour `[tool.geofix]`. Pass options with `args`, e.g. `args: [--fail-on, warning]`.
|
|
266
|
+
|
|
267
|
+
## GitHub Actions
|
|
268
|
+
|
|
269
|
+
```yaml
|
|
270
|
+
# .github/workflows/geojson.yml
|
|
271
|
+
name: GeoJSON
|
|
272
|
+
on: [push, pull_request]
|
|
273
|
+
jobs:
|
|
274
|
+
geofix:
|
|
275
|
+
runs-on: ubuntu-latest
|
|
276
|
+
steps:
|
|
277
|
+
- uses: actions/checkout@v6
|
|
278
|
+
- uses: actions/setup-python@v6
|
|
279
|
+
with:
|
|
280
|
+
python-version: "3.13"
|
|
281
|
+
- run: pip install geofix
|
|
282
|
+
- run: geofix check --format github data/
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`--format github` prints [workflow commands](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions)
|
|
286
|
+
so issues appear as annotations on the pull request diff, at the line where each feature
|
|
287
|
+
starts:
|
|
288
|
+
|
|
289
|
+
```text
|
|
290
|
+
::warning file=examples/bad.geojson,line=2,col=1,title=geofix GF212 (exterior-clockwise)::exterior ring is clockwise at $.features[0].geometry.coordinates[0]%0Ahint: Reverse the ring (ogr2ogr -lco RFC7946=YES writes the right order).
|
|
291
|
+
::error file=examples/bad.geojson,line=3,col=1,title=geofix GF220 (self-intersection)::ring crosses itself at (29.025, 41.005) at $.features[1].geometry.coordinates[0]%0Ahint: Split bow-ties into separate polygons or rebuild with shapely.make_valid.
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Python API
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
import geofix
|
|
298
|
+
|
|
299
|
+
# Validate a parsed object, a JSON string/bytes, or a file (streamed).
|
|
300
|
+
issues = geofix.check({"type": "Point", "coordinates": [3228000.0, 5009000.0]})
|
|
301
|
+
issues = geofix.check_file("parcels.geojson", ignore=["GF007"], precision=6)
|
|
302
|
+
for issue in issues:
|
|
303
|
+
print(issue.code, issue.severity, issue.line, issue.path, issue.message, issue.hint)
|
|
304
|
+
|
|
305
|
+
# Repair. The input is never modified; every modification is a Change.
|
|
306
|
+
result = geofix.fix(my_feature_collection)
|
|
307
|
+
result.data # repaired object
|
|
308
|
+
result.changes # [Change(code="GF212", path="$.features[0]...", message="rewound ...")]
|
|
309
|
+
result.issues # what is still wrong after the repair
|
|
310
|
+
|
|
311
|
+
# Repair a file feature by feature (dst may equal src; None is a dry run; "-" is stdout).
|
|
312
|
+
result = geofix.fix_file("in.geojson", "out.geojson", use_shapely=False)
|
|
313
|
+
|
|
314
|
+
# Options can be shared.
|
|
315
|
+
opts = geofix.Options(select=["GF2", "GF3"], fail_on="warning", precision=7)
|
|
316
|
+
geofix.check_file("in.geojson", opts)
|
|
317
|
+
|
|
318
|
+
# Streaming building blocks.
|
|
319
|
+
with open("big.geojsonl", encoding="utf-8") as fh:
|
|
320
|
+
for path, feature in geofix.iter_features(fh):
|
|
321
|
+
...
|
|
322
|
+
|
|
323
|
+
stats = geofix.stats_file("in.geojson") # counts, bbox, property keys, max decimals
|
|
324
|
+
geofix.RULES["GF220"].summary # rule registry
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`geofix.Validator` and `geofix.Repairer` are the incremental engines behind these functions
|
|
328
|
+
(feed them features one at a time); `geofix.Reader` produces the streaming events.
|
|
329
|
+
|
|
330
|
+
## Performance
|
|
331
|
+
|
|
332
|
+
Pure Python 3.12 on an AMD Ryzen 4000-series laptop CPU (single thread, Windows), synthetic
|
|
333
|
+
files (numbers vary with hardware; the test-suite only asserts generous limits):
|
|
334
|
+
|
|
335
|
+
| File | Size | Positions | `check` | `fix --dry-run` |
|
|
336
|
+
|---|---|---|---|---|
|
|
337
|
+
| 100 000 convex polygons (10 vertices) | 37.6 MB | 1.10 M | 5.1 s | 5.5 s |
|
|
338
|
+
| 100 000 non-convex polygons, 10% with holes | 44.1 MB | 1.37 M | 7.9 s | 8.2 s |
|
|
339
|
+
| one ring, 1 000 000 vertices (ellipse) | 25.8 MB | 1.00 M | 4.6 s | 4.6 s |
|
|
340
|
+
| one ring, 1 000 000 vertices (wavy, 1 000 lobes) | 25.8 MB | 1.00 M | 8.1 s | 7.9 s |
|
|
341
|
+
|
|
342
|
+
Repairing every feature of the 100 000-polygon file (all clockwise and over-precise: 200 000
|
|
343
|
+
changes) and writing the result takes about 21 s; a second run finds nothing to do in 5.4 s.
|
|
344
|
+
|
|
345
|
+
How: positions are validated with C-level `map`/`itemgetter` passes; the precision test is a
|
|
346
|
+
regex over the `repr` of a whole coordinate list (exact `round()` only runs when it matches);
|
|
347
|
+
convex rings skip the segment search; larger rings are split into x/y-monotone chains (whose
|
|
348
|
+
own segments cannot meet) and only chains with overlapping boxes are bisected against each
|
|
349
|
+
other, as in GEOS, with a uniform grid for zig-zag data. Memory is bounded by the largest
|
|
350
|
+
single feature, not the file.
|
|
351
|
+
|
|
352
|
+
## How geofix compares
|
|
353
|
+
|
|
354
|
+
| | geofix | [@mapbox/geojsonhint](https://github.com/mapbox/geojsonhint) | [geojson](https://pypi.org/project/geojson/) (Python) | [shapely](https://pypi.org/project/shapely/) `is_valid` / `make_valid` | `ogr2ogr -lco RFC7946=YES` |
|
|
355
|
+
|---|---|---|---|---|---|
|
|
356
|
+
| Runtime | Python, no required deps | Node.js | Python | Python + GEOS (bundled in wheels) | GDAL |
|
|
357
|
+
| RFC 7946 structure checks | yes | yes | partial (`is_valid` / `errors()`) | no (geometry only) | no (it converts) |
|
|
358
|
+
| Winding order (right-hand rule) | check + fix | check | no | no (`orient` can fix) | writes CCW/CW |
|
|
359
|
+
| Self-intersections, holes | check; fix bow-ties, rest via shapely | no | no | yes (GEOS) | no |
|
|
360
|
+
| bbox, ids, foreign members, `crs` | yes | partly | no | no | writes a new file |
|
|
361
|
+
| Projected-CRS / swapped-axes hints | yes | no | no | no | n/a (reprojects when told the CRS) |
|
|
362
|
+
| Reports what was changed | yes (per change) | n/a | n/a | no | no |
|
|
363
|
+
| Streams large files / GeoJSONSeq | yes | no | no | n/a | yes |
|
|
364
|
+
| CI output (exit codes, GitHub annotations) | yes | CLI only | no | no | no |
|
|
365
|
+
|
|
366
|
+
Use shapely/GEOS when you need full OGC validity semantics on arbitrary geometries (geofix
|
|
367
|
+
calls `shapely.make_valid` for cases it cannot repair itself), and GDAL when you need to
|
|
368
|
+
reproject or convert formats; geofix is the lightweight gate that tells you which of those
|
|
369
|
+
you need, in a form CI understands.
|
|
370
|
+
|
|
371
|
+
## Development
|
|
372
|
+
|
|
373
|
+
```console
|
|
374
|
+
python -m pip install -e ".[dev,shapely]"
|
|
375
|
+
python -m pytest
|
|
376
|
+
python -m ruff check . && python -m ruff format --check src tests
|
|
377
|
+
python -m mypy src
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## License
|
|
381
|
+
|
|
382
|
+
MIT, see [LICENSE](LICENSE). Citation metadata: [CITATION.cff](CITATION.cff).
|