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.

ISOXML to Trimble Shapefile conversion flow Five stages: parse TreatmentZone polygons and scaled rate from TASKDATA.XML, unscale the DDI integer to kg per ha, set the ten-character RATE field, reproject WGS84 to UTM, write Shapefile and read back to verify. Parse TZN TASKDATA.XML WGS84 polygons Unscale rate DDI 0006 ÷100 → kg/ha Field name RATE (≤10 ch) DBF limit Reproject 4326 → UTM field zone Write + verify .shp + .prj read-back assert

Prerequisites

Only lxml differs from the parent Converting Prescriptions Between Controller Formats cluster:

TEXT
geopandas==0.14.4
shapely==2.0.4
pyproj==3.6.1
lxml==5.2.1
numpy==1.26.4

Install with:

BASH
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.XML file whose task uses polygon TreatmentZone (TZN) elements with ProcessDataVariable (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:32615 for UTM 15N). Do not assume — confirm it against the field location.

Step-by-Step

Mapping a richer format onto a flatter one A table mapping four ISOXML concepts onto their shapefile equivalents: treatment zone polygons map cleanly to features, rates become numeric columns whose units survive only in the column name, product definitions collapse to a string, and task metadata has no equivalent at all. ISOXML concept Shapefile equivalent Where it is lost Treatment zone polygon geometry A feature Nothing lost Rate with declared units attribute A numeric column Units live only in the column name Product definition referenced element A text attribute, at best Identity becomes a string Task and timing metadata wrapper No equivalent Dropped entirely

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:

PYTHON
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 PDV element 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 .prj sidecar loses the CRS. Trimble needs the projection to place the prescription. geopandas.to_file writes the .prj automatically, but if you copy only the .shp to a USB stick the display gets undefined coordinates. Always transfer the full .shp/.shx/.dbf/.prj set.
  • Grid-based tasks return no polygons. If the assert records line fires, the task stores rates as a raster grid, not TreatmentZone polygons. 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_valid flags. Repair with shapely.make_valid or a zero-width buffer before writing, as covered in debugging shapefile geometry errors in QGIS and Python.
The two habits that make a lossy conversion safe Two panels on converting ISOXML to shapefile: attribute column names must carry the units within a ten-character budget, where a single letter distinguishes litres per hectare from litres per acre; and reading the written file back to compare areas and total product catches both truncation and unit slips. Column names carry the units RATE_KGHA truncated to RATE_KGHA Ten characters is the whole budget. RATE_LHA and RATE_LAC differ in one letter. Document the mapping outside the file. Round-trip before sending Read the shapefile back, not the source Compare zone areas within a tolerance. Compare total product against the ISOXML. Catches truncation and unit slips together.

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.