ISOXML vs Shapefile for Field Boundary Exchange: A Decision Guide
Problem statement
When a field boundary has to leave one system and arrive intact in another, the format you serialize it in decides how much can silently break in transit. A canonical polygon that lives cleanly in your database has to be handed to a tractor’s task controller before a spray pass, or exported to an agronomist’s desktop GIS for planning, or archived for a regulator. The two formats that dominate that exchange are ISO 11783-10 ISOXML — the payload inside a TASKDATA.XML archive — and the ESRI Shapefile, the lingua franca of every GIS on the planet. They are not interchangeable, and choosing the wrong one for a given hop is how a boundary drifts, loses its coordinate reference, or arrives with no way to link back to the task it belongs to.
This guide compares the two across the five dimensions that actually matter for a boundary in motion — coordinate reference handling, multipart geometry and holes, attribute richness, task-data linkage, and tooling — then recommends a format per use case. It assumes you already have a normalized, WGS84, OGC-wound geometry in hand; producing that from a raw export is the job of parsing ISOXML and shapefile field boundaries, and keeping successive versions of it reconciled is the job of field boundary synchronization. Here the concern is narrower: given a boundary that is already correct, which container carries it to the next system with the least loss.
The stakes are concrete. Emit a shapefile without its .prj sidecar and the receiving system has no idea whether the numbers are degrees or metres. Emit ISOXML with the wrong LSG type codes and a field with a pond becomes a self-intersecting blob. Pick a format that cannot express the link between a boundary and its task and you sever the thread an audit later needs. The decision is small but load-bearing.
Comparison matrix
The table below is the short answer; the sections after it explain the reasoning. Every row is a place where the two formats diverge in a way that changes ingestion code downstream.
| Dimension | ISO 11783-10 ISOXML | ESRI Shapefile |
|---|---|---|
| CRS handling | WGS84 (EPSG:4326) mandated by the standard; no CRS metadata travels with the file |
Arbitrary CRS declared in a .prj sidecar; undefined and unsafe if the sidecar is missing |
| Multipart & holes | Explicit: LSG type 1 = exterior ring, type 2 = interior hole; parts named individually |
Implicit: parts concatenated in one record; hole-vs-island decided only by ring winding |
| Attribute richness | Sparse, typed, code-based (PFD, PLN, LSG attributes); geared to machine control |
Rich .dbf table with named columns, but 10-char field names and legacy codec limits |
| Task-data linkage | Native — a boundary lives beside its TSK, device, and product references in one archive |
None built in; linkage must be reconstructed by joining on a shared key |
| Tooling & ecosystem | Niche: OEM controllers, lxml plus custom code; few off-the-shelf libraries |
Ubiquitous: pyshp, GDAL/OGR, QGIS, PostGIS — every GIS reads it |
CRS handling
This is the sharpest divide. ISO 11783-10 fixes the coordinate reference system at WGS84, so an ISOXML PNT block never carries CRS metadata — the standard is the metadata. That removes a whole class of ambiguity: there is no .prj to lose. A shapefile inverts the trade-off. It can carry any projection, which is powerful for a UTM-based agronomy workflow, but the CRS lives in a separate .prj sidecar file that travels independently and is routinely dropped when someone emails a zipped .shp and .dbf alone. A shapefile without its .prj is not “probably WGS84” — it is undefined, and guessing is how a field lands in the ocean.
Multipart geometry and holes
A field split by a waterway, or one with a pond cut out of the middle, needs both formats to agree on which ring is solid land and which is a void. ISOXML is explicit about it: each LSG element carries a type code, 1 for an exterior boundary and 2 for an interior hole, so the topology is stated rather than inferred. The shapefile format concatenates all rings into one record and leaves hole-versus-island entirely to ring winding direction. That works when the producer honors the winding convention and fails silently when it does not, which is why a shapefile round-trip should always re-run winding enforcement.
Attribute richness
The shapefile wins on breadth. Its .dbf sidecar is a full attribute table with arbitrary named columns, so grower IDs, planted acreage, and agronomic notes ride along with the geometry. The catch is the format’s age: field names are capped at ten characters and text is stored in a legacy codepage unless the producer set an encoding hint, so FARM_IDENTIFIER silently becomes FARM_IDENT and multi-byte parcel names can corrupt. ISOXML attributes are sparse and code-based by comparison, tuned for what a controller needs to actuate rather than for human-readable records.
Task-data linkage
This is where ISOXML is unbeatable. Because a boundary (PFD) lives in the same TASKDATA.XML archive as the task (TSK), the device, and the product references, the link between “this field” and “this operation on this field with this product” is native and unbroken. A shapefile is geometry plus a flat attribute table and nothing more; any linkage to a task has to be reconstructed downstream by joining on a shared key, and every such join is a place the thread can be lost.
Tooling and ecosystem
The shapefile is read by essentially everything — pyshp, GDAL/OGR, QGIS, PostGIS, ArcGIS — so it is the safe choice whenever a human or a general GIS is on the receiving end. ISOXML tooling is comparatively niche: you will parse it with lxml and custom code, and the consumers that speak it fluently are OEM task controllers, not desktop analysts.
Recommendation: choose by the receiving system
- Choose ISOXML when the consumer is a machine. If the boundary is going to a task controller for a variable-rate or spray pass, ISOXML is the right container because the receiver already speaks it, the CRS ambiguity disappears, and the boundary arrives linked to its task and product. This is the format to emit when exporting prescription maps to task controllers is the next step.
- Choose Shapefile when the consumer is a GIS or a human analyst. For desktop agronomy, mapping, or handing a boundary to a third party who “just needs the polygon,” the shapefile’s universal readability and rich attribute table win. Emit the
.prjevery time, without exception. - Choose Shapefile for regulatory archival, with a caveat. Regulators and their contractors overwhelmingly consume shapefiles, so it is the pragmatic archive format — but pin the CRS in the
.prjand store the source EPSG in your own audit ledger rather than trusting the sidecar to survive. - When in doubt, keep the canonical copy internal and generate the exchange format on demand. Never let either wire format become your source of truth; derive both from one normalized geometry so a lost
.prjor a mangledLSGcode is a re-export, not a data-loss event.
Parameter reference table
The values below govern the recommended converter in the next section. Defaults assume WGS84 internal storage and mixed machine/GIS consumers.
| Parameter | Type | Recommended value | Effect on behavior |
|---|---|---|---|
target |
ExchangeTarget |
(per hop) | Selects the format. TASK_CONTROLLER → ISOXML; GIS / REGULATOR → Shapefile. Drives every downstream default. |
emit_prj |
bool |
True |
Always writes the .prj sidecar for shapefile output. Disabling it is the fastest way to ship an undefined-CRS field; the writer refuses. |
enforce_winding |
bool |
True |
Re-applies OGC winding (orient(sign=1.0)) before serialization so shapefile holes survive the round-trip and ISOXML LSG codes stay consistent. |
dbf_encoding |
str |
"utf-8" |
Encoding hint written into the .cpg sidecar for shapefile attributes. Falls back to cp437 only for legacy consumers that cannot read the hint. |
field_name_map |
dict[str, str] |
(explicit) | Maps internal attribute names to <=10-char shapefile column names, avoiding silent truncation collisions. |
include_task_ref |
bool |
True |
For ISOXML, embeds the TSK/product reference alongside the PFD. Only meaningful for the task-controller target. |
Runnable implementation
The module below encapsulates the decision as code: select_exchange_format applies the recommendation matrix, and the two encoders serialize a canonical FieldBoundary into the chosen container. It targets Python 3.10+ (uses match/case), is fully typed, and depends on pyshp, lxml, and shapely.
from __future__ import annotations
import logging
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
import shapefile # pyshp
from lxml import etree
from shapely.geometry import MultiPolygon
from shapely.geometry.polygon import orient
logger = logging.getLogger("boundary.exchange")
class ExchangeTarget(Enum):
TASK_CONTROLLER = "task_controller" # machine consumer
GIS = "gis" # desktop / analyst consumer
REGULATOR = "regulator" # statutory archive
class Format(Enum):
ISOXML = "isoxml"
SHAPEFILE = "shapefile"
@dataclass(frozen=True)
class FieldBoundary:
field_id: str
geometry: MultiPolygon # canonical WGS84, OGC-wound
task_ref: str | None # populated only when a task is linked
attributes: dict[str, str]
def select_exchange_format(target: ExchangeTarget) -> Format:
"""Apply the recommendation matrix: machines get ISOXML, humans get shapefiles."""
match target:
case ExchangeTarget.TASK_CONTROLLER:
return Format.ISOXML # native CRS + task linkage
case ExchangeTarget.GIS | ExchangeTarget.REGULATOR:
return Format.SHAPEFILE # universal tooling
raise ValueError(f"unhandled target: {target!r}")
def _wound(geom: MultiPolygon) -> MultiPolygon:
"""Re-enforce OGC winding so holes survive serialization either way."""
return MultiPolygon([orient(p, sign=1.0) for p in geom.geoms])
def write_isoxml(fb: FieldBoundary, out: Path) -> Path:
"""Emit a minimal TASKDATA.XML: PFD -> PLN -> LSG (1 exterior, 2 hole) -> PNT."""
root = etree.Element("ISO11783_TaskData", VersionMajor="4", DataTransferOrigin="1")
pfd = etree.SubElement(root, "PFD", A=fb.field_id, C=fb.attributes.get("name", ""))
if fb.task_ref: # native linkage is the whole reason to pick ISOXML
etree.SubElement(root, "TSK", A=fb.task_ref, G="1").set("B", fb.field_id)
for poly in _wound(fb.geometry).geoms:
pln = etree.SubElement(pfd, "PLN", A="1") # A=1 => boundary polygon
_write_ring(pln, poly.exterior.coords, lsg_type="1") # exterior
for hole in poly.interiors:
_write_ring(pln, hole.coords, lsg_type="2") # interior hole
out.write_bytes(etree.tostring(root, xml_declaration=True, encoding="UTF-8"))
logger.info('{"event":"isoxml_written","field_id":"%s","task":%s}',
fb.field_id, f'"{fb.task_ref}"' if fb.task_ref else "null")
return out
def _write_ring(pln: etree._Element, coords, lsg_type: str) -> None:
lsg = etree.SubElement(pln, "LSG", A=lsg_type) # A: 1 exterior, 2 interior
for lon, lat in coords: # ISO mandates WGS84; C=north/lat, D=east/lon
etree.SubElement(lsg, "PNT", A="2", C=f"{lat:.9f}", D=f"{lon:.9f}")
def write_shapefile(fb: FieldBoundary, out: Path, name_map: dict[str, str]) -> Path:
"""Emit .shp/.dbf/.prj/.cpg; the .prj and .cpg are non-negotiable."""
with shapefile.Writer(str(out), shapeType=shapefile.POLYGON) as w:
for internal, col in name_map.items():
w.field(col[:10], "C", size=64) # dBASE caps names at 10 chars
# pyshp wants exterior rings CW and holes CCW: invert OGC winding here.
rings = []
for poly in fb.geometry.geoms:
rings.append(list(orient(poly, sign=-1.0).exterior.coords))
rings.extend(list(h.coords) for h in orient(poly, sign=-1.0).interiors)
w.poly(rings)
w.record(*[fb.attributes.get(k, "") for k in name_map])
out.with_suffix(".prj").write_text(_WGS84_WKT) # CRS never travels implicitly
out.with_suffix(".cpg").write_text("UTF-8") # declare attribute encoding
logger.info('{"event":"shapefile_written","field_id":"%s","prj":true}', fb.field_id)
return out
_WGS84_WKT = (
'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],'
'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433],AUTHORITY["EPSG","4326"]]'
)
def export_boundary(fb: FieldBoundary, target: ExchangeTarget, out_dir: Path,
name_map: dict[str, str]) -> Path:
match select_exchange_format(target):
case Format.ISOXML:
return write_isoxml(fb, out_dir / "TASKDATA.XML")
case Format.SHAPEFILE:
return write_shapefile(fb, out_dir / f"{fb.field_id}.shp", name_map)
The converter refuses to write a shapefile without a .prj, always re-runs winding before serialization, and only embeds a TSK reference when the ISOXML path is taken — because the task linkage that justifies choosing ISOXML is meaningless in a shapefile.
Log patterns and observable signals
Each export emits structured JSON so an archived boundary can be traced to the format and target it was written for.
ISOXML written for a task controller, task linked:
{"event": "isoxml_written", "field_id": "PFD1", "task": "TSK7"}
Shapefile written for a GIS consumer with its projection sidecar:
{"event": "shapefile_written", "field_id": "PFD1", "prj": true}
Refusal — a shapefile export was requested but no .prj could be produced:
{"event": "export_rejected", "field_id": "PFD1", "reason": "prj_unavailable", "action": "halt_export"}
When triaging, treat any export_rejected as blocking: the receiving system got nothing, not a partial file. A run of ISOXML exports with "task": null for a target of TASK_CONTROLLER means boundaries are reaching machines unlinked to their operations — the exact thread an audit needs — and points back at the linkage step in field boundary synchronization.
Troubleshooting
- Receiving GIS shows the field in the wrong hemisphere or ocean. Root cause: the shapefile arrived without its
.prj, or lat/lon was written in the wrong axis order. Remediation: confirm the.prjwas emitted (the writer enforces this) and that coordinates are lon/lat; never let a consumer assume a default CRS. - A pond or waterway gap fills in after a shapefile round-trip. Root cause: interior-hole winding was lost because pyshp expects clockwise exteriors and counter-clockwise holes, the inverse of OGC. Remediation: keep
orient(sign=-1.0)on the shapefile path and re-enforce OGC winding on re-import. - Truncated or colliding attribute columns in the
.dbf. Root cause: internal names longer than ten characters were silently cut, mapping two fields onto one column. Remediation: supply an explicitfield_name_mapwith unique <=10-char targets and write a.cpgencoding hint. - Task controller ingests the field but cannot associate it with a product. Root cause: a shapefile was sent to a machine consumer, which carries no task linkage. Remediation: use the ISOXML path for
TASK_CONTROLLERtargets so theTSKand product references travel in the same archive. - ISOXML parses as empty on the controller. Root cause: a namespace was added to the XML, or
LSGtype codes were omitted. Remediation: keepTASKDATA.XMLnamespace-free and always setLSGAto1or2; validate against the ISOXML and shapefile parser before shipping.
Frequently asked questions
Which format should carry a boundary to a tractor task controller? ISOXML — the controller speaks it natively, the CRS is fixed at WGS84, and the boundary travels linked to its task and product in one archive.
Why is a shapefile without a .prj file dangerous? The CRS lives in a separate sidecar that is easily dropped; without it the coordinates are undefined, and guessing a datum is how a field lands in the ocean. Always emit the .prj.
How do the two formats differ on holes? ISOXML states exteriors and holes explicitly with LSG type codes; a shapefile leaves it to winding direction, so re-enforce OGC winding on every conversion.
Related
- Parsing ISOXML and shapefile field boundaries — how to decode either format into the canonical geometry this guide exports.
- Field boundary synchronization — reconciling successive versions of a boundary before it is exchanged.
Up: Equipment Telemetry Parsing · Farm Data Ingestion & Field Boundary Synchronization