Converting ISOXML to Shapefile for Trimble
TL;DR: Parse TreatmentZone polygons and their DDI-scaled rate from TASKDATA.XML, divide the scaled integer back to a real kg/ha rate, rename the rate column to a ≤ 10-character field like RATE, reproject from WGS84 to the field’s UTM zone, write with geopandas.to_file(driver="ESRI Shapefile"), and read the file back to assert the zone count, CRS, and rates all survived.
Why This Conversion Arises
A prescription generated for or exported from a John Deere / ISOBUS workflow arrives as ISOXML — an ISO 11783-10 TASKDATA.XML bundle. Trimble displays (GFX-750, TMX-2050, and the FmX/CFX lineage) do not read ISOXML; they read a rate Shapefile. So any operation running mixed iron — a Deere planter and a Trimble-guided sprayer, or a custom applicator whose fleet is Trimble — has to translate the ISOXML prescription into a Shapefile the Trimble display will accept.
Two things break silently in this translation. First, ISOXML stores rates as scaled integers keyed to a DDI (data dictionary identifier): DDI 0006 encodes mass per area in mg/m², so a 168 kg/ha rate is stored as the integer 16800. Copy that integer straight into the Shapefile and the Trimble display will attempt to apply sixteen thousand of something. Second, the dBASE .dbf under every Shapefile truncates field names to ten characters, and Trimble’s import maps to the exact field name — a longer name is silently cut and the import reads a zero rate. This guide handles both, plus the WGS84 → UTM reprojection Trimble expects, and proves the result with a read-back assert.
This is the concrete, single-path version of the general hub described in the parent guide, Converting Prescriptions Between Controller Formats.
Prerequisites
Only lxml differs from the parent Converting Prescriptions Between Controller Formats cluster:
geopandas==0.14.4
shapely==2.0.4
pyproj==3.6.1
lxml==5.2.1
numpy==1.26.4
Install with:
pip install geopandas==0.14.4 shapely==2.0.4 pyproj==3.6.1 lxml==5.2.1 numpy==1.26.4
Input requirements:
- A
TASKDATA.XMLfile whose task uses polygonTreatmentZone(TZN) elements withProcessDataVariable(PDV) rates. Grid-only tasks need vectorising first (see Gotchas). - The DDI and its scale factor for your rate. This guide assumes DDI 0006 (mass per area, mg/m²), whose integer-to-kg/ha factor is 100.
- The field’s target UTM zone as an EPSG code (e.g.
EPSG:32615for UTM 15N). Do not assume — confirm it against the field location.
Step-by-Step
Step 1 — Parse TreatmentZone geometry and scaled rate
TASKDATA.XML nests each management zone as a TZN element containing polygon vertices (PNT elements with C = longitude, D = latitude) and a PDV element whose A attribute holds the scaled integer rate. Parse with lxml and build Shapely polygons in WGS84.
Step 2 — Unscale the DDI integer to a real rate
DDI 0006 stores mass per area as mg/m². One kg/ha equals 100 mg/m², so divide the stored integer by 100 to recover kg/ha. Keep the unit token beside the number so nothing downstream guesses.
Step 3 — Set the 10-character field name
Rename the rate column to RATE. The Trimble import maps to this exact name, and staying at four characters sidesteps the dBASE 10-character truncation entirely.
Step 4 — Reproject WGS84 to the field UTM zone
ISOXML geometry is EPSG:4326. Trimble expects a projected CRS with a matching .prj; reproject to the field’s UTM zone with to_crs. Getting the zone right matters — the WGS84-to-UTM selection logic is detailed in how to convert WGS84 to UTM for farm mapping.
Step 5 — Write and verify
Write the Shapefile (which emits .shp/.shx/.dbf/.prj together), then read it back and assert parity. The complete, directly runnable script:
import numpy as np
import geopandas as gpd
from shapely.geometry import Polygon
from lxml import etree
TASKDATA = "TASKDATA/TASKDATA.XML"
OUT_SHP = "rx_trimble.shp"
TARGET_UTM_EPSG = 32615 # UTM 15N — replace with the field's zone
DDI_SCALE = 100.0 # DDI 0006: integer mg/m² -> kg/ha
RATE_FIELD = "RATE" # <= 10 chars; the name Trimble import maps to
# ── 1. Parse TreatmentZone polygons + scaled rate ─────────────────────────
tree = etree.parse(TASKDATA)
root = tree.getroot()
records = []
for zi, tzn in enumerate(root.iter("TZN"), start=1):
pdv = tzn.find(".//PDV")
if pdv is None or pdv.get("A") is None:
continue
scaled = int(pdv.get("A")) # e.g. 16800
rate_kg_ha = scaled / DDI_SCALE # -> 168.0 kg/ha
coords = [(float(p.get("C")), float(p.get("D"))) # (lon, lat)
for p in tzn.iter("PNT")]
if len(coords) >= 3:
records.append({"zone_id": zi, RATE_FIELD: round(rate_kg_ha, 1),
"geometry": Polygon(coords)})
assert records, "No polygon TreatmentZones found — is this a grid-based task?"
gdf = gpd.GeoDataFrame(records, crs="EPSG:4326")
# ── 2. Validate the common model before writing ───────────────────────────
assert gdf.geometry.is_valid.all(), "Invalid polygon(s) — repair before export"
assert (gdf[RATE_FIELD] >= 0).all(), "Negative rate — check the DDI scale factor"
assert len(RATE_FIELD) <= 10, "Rate field name exceeds the dBASE 10-char limit"
# ── 3. Reproject WGS84 -> field UTM zone ──────────────────────────────────
gdf_utm = gdf.to_crs(epsg=TARGET_UTM_EPSG)
assert gdf_utm.crs.to_epsg() == TARGET_UTM_EPSG, "Reprojection missed target CRS"
# ── 4. Write the Trimble rate Shapefile (.shp/.shx/.dbf/.prj) ──────────────
gdf_utm.to_file(OUT_SHP, driver="ESRI Shapefile")
print(f"Wrote {OUT_SHP}: {len(gdf_utm)} zones in EPSG:{TARGET_UTM_EPSG}")
# ── 5. Read-back verification ─────────────────────────────────────────────
back = gpd.read_file(OUT_SHP)
assert len(back) == len(gdf), "Zone count changed on write"
assert back.crs.to_epsg() == TARGET_UTM_EPSG, "Written CRS is not the target UTM zone"
assert RATE_FIELD in back.columns, f"{RATE_FIELD} missing — field name was truncated"
orig = gdf.sort_values("zone_id")[RATE_FIELD].to_numpy()
rt = back.sort_values("zone_id")[RATE_FIELD].to_numpy()
max_dev = float(np.abs(orig - rt).max())
assert max_dev <= 0.05, f"Rate drift {max_dev:.3f} exceeds tolerance"
print(f"Verified: rates {back[RATE_FIELD].min():.1f}–{back[RATE_FIELD].max():.1f} kg/ha, "
f"max round-trip deviation {max_dev:.3f}")
The read-back is the load-bearing check: it catches a truncated field name (the RATE_FIELD in back.columns assert), a botched reprojection (the CRS assert), and any rate that drifted during the write (the deviation assert) — the three failures that otherwise surface only when the operator loads the file on the display.
Gotchas & Edge Cases
- The DDI scale is not always 100. DDI 0006 (mg/m²) unscales by 100 to kg/ha, but seeding-rate and volume DDIs use different factors and units. Confirm the DDI in the
PDVelement against the ISO 11783 data dictionary before hard-coding a divisor — a wrong factor multiplies or divides every rate by a power of ten silently. - A missing
.prjsidecar loses the CRS. Trimble needs the projection to place the prescription.geopandas.to_filewrites the.prjautomatically, but if you copy only the.shpto a USB stick the display gets undefined coordinates. Always transfer the full.shp/.shx/.dbf/.prjset. - Grid-based tasks return no polygons. If the
assert recordsline fires, the task stores rates as a raster grid, notTreatmentZonepolygons. Vectorise the grid — each contiguous rate class becomes a polygon — then run the same pipeline. The parent guide covers this vectorisation path. - Self-intersecting rings from the source. ISOXML polygons occasionally arrive with a self-intersection that
is_validflags. Repair withshapely.make_validor a zero-width buffer before writing, as covered in debugging shapefile geometry errors in QGIS and Python.
Frequently Asked Questions
Does Trimble need the rate Shapefile in WGS84 or a projected CRS?
Trimble displays read a rate Shapefile with a matching .prj sidecar and generally expect a projected CRS, most commonly the field’s UTM zone. ISOXML stores geometry in WGS84, so the conversion must reproject explicitly. Ship the .shp, .shx, .dbf, and .prj together, because a missing .prj leaves the CRS undefined and the prescription can load offset or be rejected.
Why is my Trimble rate column empty after import?
The dBASE format behind a Shapefile truncates attribute names to ten characters, and Trimble’s import maps to the exact field name you configure. If the writer stored a longer name it was silently truncated, so the display looks for a field that no longer exists and imports a zero rate. Name the rate field ten characters or fewer, such as RATE, and confirm it in the read-back.
How do I handle a grid-based ISOXML prescription instead of polygons?
Some TASKDATA.XML tasks store rates as a raster grid referenced by a .bin file plus a bounding box rather than explicit polygon zones. Vectorise the grid first so each contiguous rate class becomes a polygon, then feed those polygons through the same conversion. The polygon parser here returns nothing for a pure grid task, which is the signal to switch to grid vectorisation.
Parent Guide
This guide is part of Converting Prescriptions Between Controller Formats — see there for the full any-to-any conversion model, the format compatibility matrix, and unit-conversion tables that this single path draws on.
Related
- Converting Prescriptions Between Controller Formats — the format-agnostic hub with the common model, unit tables, and CRS rules
- Variable Rate Export to ISOXML — how the TASKDATA.XML you are reading here is produced in the first place
- Debugging Shapefile Geometry Errors in QGIS and Python — repairing invalid rings before the Trimble Shapefile is written