Sandbox field boundaries: GeoJSON and a joinable registry

Two fictional closed polygons and a two-row CSV registry, with executable geometry/ID checks and joins to all 144 sensor and 24 scouting records.

License: cc-by-4.0

synthetic, geojson, interoperability, field-id

Overview

# Sandbox field boundaries: GeoJSON and a joinable registry

## Goal and required downloads

Check geometry parsing and lossless field-ID joins in an isolated sandbox. Download `sandbox-fields.geojson`, `field-registry.csv`, `data-dictionary-v2.json` and `license.txt`. For the full join exercise, copy only `sensor-observations.csv` from [sensors](/hub/agridigi/synthetic-sensor-timeseries) and `scouting-observations.csv` from [scouting labels](/hub/agridigi/synthetic-scouting-labels) into the same folder. Keep each capsule's dictionary/license with its own original download. Save the script as `check_fields.py` and run `python3 check_fields.py`; it uses only the Python 3 standard library.

## Geometry and procedure

The file is an RFC 7946 FeatureCollection with two Polygon features. WGS84 decimal-degree positions are [longitude, latitude]. A is longitude 30.00–30.01/latitude 40.00–40.01; B is 30.03–30.04/40.02–40.03. These coordinates are invented; overlap with a real place proves no ownership or measured area. Every exterior ring has five positions including its repeated closing position. `demo-crop` is a non-agronomic placeholder.
First validate geometry and unique IDs; then join observations by `field_id`, rejecting unknown IDs instead of dropping rows. The geographic-range check alone cannot detect every latitude/longitude swap; inspect the declared bounds when plotting.

```python
import csv, json
from collections import Counter
from pathlib import Path

root = Path(".")
geo = json.loads((root / "sandbox-fields.geojson").read_text(encoding="utf-8"))
with (root / "field-registry.csv").open(newline="", encoding="utf-8") as f:
    registry_rows = list(csv.DictReader(f))
registry = {r["field_id"]: r for r in registry_rows}
assert geo["type"] == "FeatureCollection"
assert len(geo["features"]) == len(registry_rows) == len(registry) == 2
ids = set()
for feature in geo["features"]:
    p, g = feature["properties"], feature["geometry"]
    fid = feature["id"]
    assert fid == p["field_id"] and fid in registry and fid not in ids
    ids.add(fid)
    assert p["is_synthetic"] is True and registry[fid]["is_synthetic"] == "true"
    assert p["field_name"] == registry[fid]["field_name"]
    assert p["crop"] == registry[fid]["crop"] == "demo-crop"
    assert g["type"] == "Polygon" and len(g["coordinates"]) == 1
    ring = g["coordinates"][0]
    assert len(ring) == 5 and ring[0] == ring[-1]
    assert len({tuple(point) for point in ring[:-1]}) == 4
    assert all(len(point) == 2 and -180 <= point[0] <= 180 and -90 <= point[1] <= 90 for point in ring)
assert ids == set(registry) == {"sandbox-field-a", "sandbox-field-b"}
print("polygons=2; registry_rows=2; closed_rings=2")

for filename, expected, per_field in (("sensor-observations.csv", 144, 72),
                                      ("scouting-observations.csv", 24, 12)):
    with (root / filename).open(newline="", encoding="utf-8") as f:
        observations = list(csv.DictReader(f))
    assert len(observations) == expected
    assert all(r["field_id"] in registry for r in observations)
    counts = Counter(r["field_id"] for r in observations)
    assert counts == {fid: per_field for fid in registry}
    joined = [{**r, "field_name": registry[r["field_id"]]["field_name"]} for r in observations]
    assert len(joined) == expected
    print(filename + ": matched=" + str(len(joined)) + "; unknown_fields=0")
```

## Expected output and worked example

`polygons=2; registry_rows=2; closed_rings=2`
`sensor-observations.csv: matched=144; unknown_fields=0`
`scouting-observations.csv: matched=24; unknown_fields=0`
A sensor record for `sandbox-field-a` receives `field_name=Sandbox A`; all 72 sensor and 12 scouting rows for that ID join once. Do not join on a translated display name. In a sandbox map, frame both declared coordinate ranges and confirm two distinct shapes. The script checks structure/joins, not topological validity for arbitrary polygons.

## Next action, privacy, provenance and license

Add an unknown field ID to a local copy and confirm the assertion fails; retain the original download for comparison. Never import these polygons as real operational fields, sampling points, legal boundaries or control zones. AgriDigi authored the fictional geometry; [RFC 7946](https://www.rfc-editor.org/rfc/rfc7946.html) defines the format and is linked, not republished. No real locations or personal records are supplied. Original fixture/text is [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/): credit AgriDigi, link source/license and note changes; external references retain their terms.

Dataset

Rows: 2

Columns: field_id, field_name, crop, is_synthetic

  • license.txt · text/plain · 558 bytes
  • sandbox-fields.geojson · application/geo+json · 1426 bytes
  • field-registry.csv · text/csv · 123 bytes
  • data-dictionary-v2.json · application/json · 8754 bytes
  • readme-v2-en.txt · text/plain · 4622 bytes
  • readme-v2-tr.txt · text/plain · 4711 bytes
  • readme-v2-de.txt · text/plain · 4346 bytes
  • readme-v2-es.txt · text/plain · 4538 bytes
  • readme-v2-fr.txt · text/plain · 4421 bytes
  • readme-v2-it.txt · text/plain · 4213 bytes
  • readme-v2-az.txt · text/plain · 4210 bytes
  • readme-v2-ru.txt · text/plain · 5205 bytes
  • readme-v2-sr.txt · text/plain · 4149 bytes
  • readme-v2-ar.txt · text/plain · 4875 bytes
  • validate-v2.txt · text/plain · 2108 bytes
  • geometry-audit-v2.csv · text/csv · 243 bytes
  • join-audit-v2.csv · text/csv · 124 bytes
  • expected-checks-v2.json · application/json · 205 bytes

View publisher @agridigi

Explore AgriDigi Hub