Skip to main content

Spatial Encoder serialization strategies

View Markdown versionEdit this page on GitHub

Next version — This documentation describes a major version that is not yet the current stable release of spatial-encoder.

View the library roadmap

Spatial Encoder encodes spatial-types objects into text or binary data. All strategies implement StrategyInterface and expose encode(SpatialInterface $spatial): string. Encoder delegates conversion to the selected strategy. Conversion does not perform any reprojection.

Choosing a Strategy​

This table describes the capabilities of the repository's current implementation.

Class.OutputSRID in outputDimensions writtenUsage
GeoJsonStrategyGeoJSON geometry textNoXY, XYZGeometries encoding according to RFC 7946
WktTextStrategyWKT Well Known TextNoXY, XYZ, XYM, XYZMDisplay, text exchanges, functions accepting WKT
EwktTextStrategyEWKT Extended Well Known Text.Yes, when nonzeroXY, XYZ, XYM, XYZMText exchanges that include the spatial reference
WkbBinaryStrategyISO WKB Well Known Binary.NoXY, XYZ, XYM, XYZMExchanges with a standard WKB consumer
EwkbBinaryStrategyEWKB Extended Well Known BinaryYes, when nonzeroXY, XYZ, XYM, XYZMExchanges with a consumer of the extended PostGIS format
MySQLBinaryStrategyInternal MySQL binary formatYes, as a prefixXYIntegrations expecting spatial values in MySQL's internal format

The binary and WKT/EWKT strategies support Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, and geometry collections. WKB, EWKB, WKT, and EWKT also support Triangle and PolyhedralSurface. Classes in the Geometry and Geography families use the same output type names; for example, GeographyCollection becomes GEOMETRYCOLLECTION.

Common Usage​

The examples in the following sections reuse the imports, point, and encoder below. The SRID is passed to the spatial object's constructor.

require 'vendor/autoload.php';

use LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\Point;
use LongitudeOne\SpatialEncoder\Strategy\EwkbBinaryStrategy;
use LongitudeOne\SpatialEncoder\Strategy\EwktTextStrategy;
use LongitudeOne\SpatialEncoder\Strategy\MySQLBinaryStrategy;
use LongitudeOne\SpatialEncoder\Strategy\WkbBinaryStrategy;
use LongitudeOne\SpatialEncoder\Strategy\WktTextStrategy;
use LongitudeOne\SpatialEncoder\Encoder;

$point = new Point(1, 2, 4326);
$encoder = new Encoder(new WktTextStrategy());

echo $encoder->encode($point);
// POINT (1 2)

The strategy can also be called directly: (new WktTextStrategy())->encode($point). Binary outputs are PHP byte strings. bin2hex() is used to display them; it is not part of their serialization format.

WKT — WktTextStrategy​

This strategy produces a readable representation with parentheses and space-separated coordinates. It preserves X Y order, followed by Z and M when present. It writes the Z, M, or ZM markers, including for empty values such as POINT Z EMPTY. Collections can be nested.

All currently available concrete types are covered, including their empty variants. Polyhedral surfaces are available only in XYZ and XYZM. The SRID is omitted, as required by WKT; output starting with SRID=4326; would be EWKT, which this strategy does not produce.

Numeric formatting uses json_encode() and PHP's serialize_precision setting. Keeping its default value of -1 produces a short representation that preserves floating-point values when read back. A non-finite coordinate throws a JsonException.

References: GEOS WKT syntax and examples, PostGIS WKT conversion: ST_AsText.

EWKT — EwktTextStrategy​

This strategy extends WktTextStrategy. It adds SRID=<value>; before the WKT when the object's SRID is nonzero. A default or explicit SRID of zero produces plain WKT with no prefix.

$encoder->setStrategy(new EwktTextStrategy());
echo $encoder->encode($point);
// SRID=4326;POINT (1 2)

All geometry types, coordinate ordering, dimension markers, empty values, and numeric formatting are inherited from WKT. For example, an empty XYZM point with SRID 4326 produces SRID=4326;POINT ZM EMPTY. For collections, the prefix appears only once, before the outermost geometry; nested members retain their WKT representation without additional SRID prefixes.

The strategy performs no coordinate transformation. Its dimension markers follow WktTextStrategy, so the text can differ from PostGIS's own formatting.

Reference: PostGIS EWKT output: ST_AsEWKT.

WKB — WkbBinaryStrategy​

This strategy writes the byte-order indicator, the type identifier, then the coordinates and counts required for composite objects. It preserves X Y order and ignores the SRID. The recipient must therefore know the reference system separately.

$encoder->setStrategy(new WkbBinaryStrategy());
echo bin2hex($encoder->encode($point));
// 0101000000000000000000f03f0000000000000040

The output follows ISO WKB in little-endian byte order on every platform. It preserves X Y, followed by Z and M when present. Type identifiers use +1000 for XYZ, +2000 for XYM, and +3000 for XYZM; these are ISO offsets, not EWKB flags. Base identifiers are 1–7 for the standard geometries, 15 for PolyhedralSurface, and 17 for Triangle.

All concrete Geometry/Geography classes are supported. Polyhedral surfaces are available in XYZ and XYZM. Composite members carry their own WKB headers; polygon and triangle rings contain only point counts and coordinates. The SRID is omitted from both the outer geometry and its members.

$pointZ = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\Point(1, 2, 3, 4326);
echo bin2hex($encoder->encode($pointZ));
// 01e9030000000000000000f03f00000000000000400000000000000840

An empty point contains one IEEE-754 quiet NaN per ordinate (000000000000f87f in little-endian hexadecimal), preserving its dimensional type identifier. Other empty types contain a zero member or ring count. Nested empty members are retained. A consumer must support the corresponding ISO dimensions and geometry types to read this output.

References: GEOS WKB structure, PostGIS WKB conversion: ST_AsBinary, GDAL geometry type identifiers.

EWKB — EwkbBinaryStrategy​

This strategy uses the PostGIS extension of WKB to include the SRID. When the SRID is nonzero, it sets the type's 0x20000000 flag and writes the SRID after the type. An SRID of zero omits this field and flag. Coordinate order is X Y, followed by Z and M when present. Dimension flags remain present even with a zero SRID: 0x80000000 for Z, 0x40000000 for M, or both for ZM. These flags differ from ISO WKB dimensional offsets.

$encoder->setStrategy(new EwkbBinaryStrategy());
echo bin2hex($encoder->encode($point));
// 0101000020e6100000000000000000f03f0000000000000040

All 68 concrete Geometry/Geography classes are supported in their available dimensions, including triangles and polyhedral surfaces. Empty points contain one quiet NaN per ordinate; other empty geometries contain a zero count. All integers and coordinates are explicitly little-endian.

The SRID is written only in the outermost geometry, including when empty. Members of multi-geometries, collections, and polyhedral surfaces inherit it; their individual headers retain their type and dimension flags. This also applies to nested collections. Earlier versions repeated the SRID in some multi-geometry members; output now follows PostGIS's single outer SRID.

For example, POINT Z (1 2 3) with SRID 4326 produces 01010000a0e6100000000000000000f03f00000000000000400000000000000840. Curved geometries and TIN remain unsupported because the current dependency provides no concrete classes for them.

References: GEOS EWKB flags and structure, PostGIS ST_AsEWKB documentation.

Internal MySQL Format — MySQLBinaryStrategy​

This strategy writes a four-byte SRID prefix followed by a binary representation of the geometry. This prefix belongs to MySQL's internal format: do not pass this output unchanged to a function that expects WKB alone.

Coordinates are always written in X Y order: longitude then latitude for geographic objects, including SRID 4326. The SRID appears once as a prefix; collection members preserve the same coordinate order without extra prefixes.

MySQL's SQL constructors interpret geographic input using the SRID axis order by default (latitude then longitude for 4326). To supply longitude-first WKT, use ST_GeomFromText('POINT(1 2)', 4326, 'axis-order=long-lat'). This input convention differs from the internal storage order.

Behavior change (#5): earlier versions swapped coordinates for SRIDs classified as latitude-first. That swap and the unused SRID axis-order lookup resources are removed. Callers that previously reversed their coordinates to compensate should now pass X/longitude first and Y/latitude second. Changing the encoder does not repair previously stored coordinates.

$encoder->setStrategy(new MySQLBinaryStrategy());
echo bin2hex($encoder->encode($point));
// e61000000101000000000000000000f03f0000000000000040

Only XY objects are accepted. Z, M, and ZM inputs throw UnsupportedDimensionException, including empty collections with those dimensions. Empty points, lines, polygons, and multi-geometries throw UnsupportedSpatialTypeException; only XY GeometryCollection (including GeographyCollection) may be empty. These checks also apply to nested members.

Reference: MySQL 8.4 spatial formats and internal storage.

Current Limitations and Test Examples​

The MySQL encoder rejects additional dimensions and unsupported empty values. Use XY objects, with empty values limited to geometry collections. WKB, EWKB, WKT, and EWKT support additional dimensions and other empty geometry types.

The MySQL strategy declares little-endian byte order (01) and explicitly uses it for SRIDs, type identifiers, counts, and coordinates. Its output is independent of the machine's native byte order.

Curved geometries and TIN have no concrete classes in the current dependency and are not implemented by WKB. Unsupported types throw an UnsupportedSpatialTypeException. An unrecognized spatial interface may throw an UnsupportedSpatialInterfaceException.

The source repository includes explicit EWKB, WKB and WKT test examples and a testing guide. To add a format, implement StrategyInterface and pass the new strategy to Encoder.

GeoJSON — GeoJsonStrategy​

This strategy supports Geometry and Geography Points, LineStrings, MultiPoints, MultiLineStrings, Polygons, MultiPolygons, and collections in XY and XYZ, including EMPTY. Types outside the seven RFC 7946 geometry types are rejected. Feature and FeatureCollection composition belongs to the consuming application.

$encoder->setStrategy(new \LongitudeOne\SpatialEncoder\Strategy\GeoJsonStrategy());
echo $encoder->encode($point);
// {"type":"Point","coordinates":[1,2]}

XYZ produces positions [X,Y,Z]; EMPTY XY and XYZ produce {"type":"Point","coordinates":[]}. No artificial dimension metadata is added. Measured XYM and XYZM inputs throw UnsupportedDimensionException, even when EMPTY: M is neither discarded nor written as altitude or a fourth ordinate.

The caller supplies WGS 84 longitude/latitude in degrees and, when present, ellipsoidal height in metres. The encoder preserves the coordinates without reprojection, axis swapping, or unit conversion. It does not validate the reference using SRID or authority metadata: zero, unknown, and non-4326 identifiers are not reasons for rejection and are not serialized as a coordinate-system extension. An absent reference uses the model's default SRID of zero.

UnsupportedSpatialTypeException is now explicitly public, with its existing \Exception inheritance and constructor unchanged. It implements ExceptionInterface and reports a type unsupported by the selected strategy, including Triangle instead of silently encoding it as Polygon. UnsupportedSpatialInterfaceException remains internal and unchanged.

The shared public UnsupportedGeometryStructureException extends \InvalidArgumentException and implements ExceptionInterface; subsequent geometry contributions use it for the approved incompatible structures. A non-empty LineString with only one position throws this exception. The concrete model accepts singleton lines, but prevents EMPTY member points and mismatched position dimensions at construction; fixtures do not bypass these invariants. JsonEncodingException extends \RuntimeException, implements ExceptionInterface, and wraps an encoding failure with the original \JsonException as previous. No partial JSON is returned. The installed spatial-types model accepts non-finite floats through Geometry Point constructors; these encounter the ordinary JSON encoding failure path, without a separate encoder validation policy.

Numeric encoding uses PHP's serialize_precision setting; retain its default -1 for floating-point round trips. Whitespace and object-property ordering are not part of a canonical serialization contract.

LineStrings retain every position in its supplied order, including Z:

$line = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\LineString([[1, 2, 3], [4, 5, 6]]);
echo $encoder->encode($line);
// {"type":"LineString","coordinates":[[1,2,3],[4,5,6]]}

EMPTY XY and XYZ LineStrings produce {"type":"LineString","coordinates":[]}. XYM and XYZM lines are rejected even when EMPTY. LineStrings crossing the antimeridian, such as [[170,45],[-170,45]], are encoded unchanged, without cutting or rejection. This deliberately does not apply the SHOULD in RFC 7946 section 3.1.9: consumers may interpret or display an uncut line as crossing the long way around the globe. Callers must perform any cutting required by their consumers before encoding.

MultiPoints preserve the number and order of their positions, including duplicates and Z. A singleton remains a MultiPoint rather than becoming a Point:

$multiPoint = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\MultiPoint([[1, 2, 3], [4, 5, 6]]);
echo $encoder->encode($multiPoint);
// {"type":"MultiPoint","coordinates":[[1,2,3],[4,5,6]]}

An XY or XYZ MultiPoint with no members produces {"type":"MultiPoint","coordinates":[]}. Measured MultiPoints are rejected even with no members. References are omitted and coordinates are unchanged, under the same caller responsibilities described above.

The approved MultiPoint contract rejects any EMPTY Point member with UnsupportedGeometryStructureException, including an aggregate made entirely of EMPTY members. No member is omitted or replaced. The required spatial-types 0.0.1-alpha.2 model supports constructing these aggregates, so the encoder rejects them through its public structural exception. An empty aggregate with no members remains supported.

MultiLineStrings preserve each supplied line, its position order and Z, and the order of the members. A single member remains wrapped as a MultiLineString:

$multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\MultiLineString([
[[1, 2], [3, 4]],
[[5, 6], [7, 8]],
]);
echo $encoder->encode($multiLine);
// {"type":"MultiLineString","coordinates":[[[1,2],[3,4]],[[5,6],[7,8]]]}

An XY or XYZ MultiLineString with no members produces {"type":"MultiLineString","coordinates":[]}. Any EMPTY LineString member, including an aggregate containing only EMPTY members, throws UnsupportedGeometryStructureException. A member with only one position also throws that exception, as a GeoJSON line requires at least two positions. The model permits these line members; the encoder rejects them without omitting, flattening, or repairing them, and never returns partial output or nested empty coordinate arrays. XYM and XYZM are rejected even with no members.

Antimeridian-crossing members are preserved unchanged under the deliberate non-application of RFC 7946 section 3.1.9 described above. The same caller responsibilities, reference omission, and JSON failure contract apply.

Polygons preserve exterior and interior ring order, all positions, and Z:

$polygon = new \LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\Polygon([
[[0, 0], [4, 0], [4, 4], [0, 4], [0, 0]],
[[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]],
]);
echo $encoder->encode($polygon);
// {"type":"Polygon","coordinates":[[[0,0],[4,0],[4,4],[0,4],[0,0]],[[1,1],[1,2],[2,2],[2,1],[1,1]]]}

EMPTY XY and XYZ Polygons produce {"type":"Polygon","coordinates":[]}. XYM and XYZM are rejected even when EMPTY. The same reference omission and JSON failure contracts apply to Polygons.

Every exterior ring must have strictly positive signed XY area (counterclockwise) and every interior ring strictly negative signed XY area (clockwise). Incompatible orientation, including zero signed area, throws UnsupportedGeometryStructureException. The encoder never reverses, closes, or repairs a ring and never returns partial output. Z is preserved but does not participate in the orientation calculation. The signed-area algorithm and zero-area rejection are the approved LongitudeOne encoding policy; RFC 7946 section 3.1.6 requires winding but does not prescribe that algorithm or explicitly prescribe zero-area rejection.

The installed model already enforces at least four positions, closure, and the absence of consecutive duplicate positions. It permits collinear and crossed rings. The encoder rejects a crossed ring if its signed area is zero, but does not separately check simplicity, self-intersections, hole containment, or other topology. A nonzero signed area is not proof of topological validity; the caller remains responsible for that validation.

Antimeridian-crossing Polygon coordinates are preserved without unwrapping, rejection, or cutting; orientation uses the supplied XY coordinates. This deliberately does not apply the SHOULD in RFC 7946 section 3.1.9. Consumers may display an uncut polygon across the long way around the globe, so callers must perform any required cutting before encoding.

MultiPolygons preserve Polygon order, ring order, position order, and Z. A singleton remains a MultiPolygon. Every member uses the same Polygon orientation checks above, with no second validation policy:

$multiPolygon = new \LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\MultiPolygon([
[[[0, 0], [2, 0], [0, 2], [0, 0]]],
[[[3, 0], [5, 0], [3, 2], [3, 0]]],
]);
echo $encoder->encode($multiPolygon);
// {"type":"MultiPolygon","coordinates":[[[[0,0],[2,0],[0,2],[0,0]]],[[[3,0],[5,0],[3,2],[3,0]]]]}

An XY or XYZ MultiPolygon with no members produces {"type":"MultiPolygon","coordinates":[]}. Any EMPTY Polygon member throws UnsupportedGeometryStructureException, including when all members are EMPTY. An incompatible exterior or interior ring also rejects the whole conversion. No member is omitted, repaired, reoriented, or replaced, and no partial output is returned. Measured XYM and XYZM aggregates are rejected even with no members.

The same reference omission, caller responsibilities, and JSON failure contract apply. Antimeridian-crossing members retain their supplied coordinates under the deliberate non-application of RFC 7946 section 3.1.9 described above, with the same possible long-way-around interpretation by consumers.

GeometryCollection behavior is documented below; the earlier incremental availability note is superseded by that implementation.

References: RFC 7946 geometry objects and positions, MultiPoint positions, MultiLineString members, Polygon rings, MultiPolygon members, coordinate reference system, non-extensible types.

GeometryCollections recursively preserve each member's type, coordinates, order, and nesting. GeographyCollection uses the same GeoJSON GeometryCollection type:

$collection = new \LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\GeometryCollection(4326, [
new \LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\Point(1, 2, 4326),
new \LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\LineString([[3, 4], [5, 6]], 4326),
]);
echo $encoder->encode($collection);
// {"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[1,2]},{"type":"LineString","coordinates":[[3,4],[5,6]]}]}

A collection with no members produces {"type":"GeometryCollection","geometries":[]}. EMPTY geometry members retain their own empty coordinates arrays; nested collections retain their geometries arrays. No coordinates member is added to a collection. XY and XYZ are supported, preserving Z; XYM and XYZM are rejected even when EMPTY. Reference metadata is omitted throughout, under the caller responsibilities above.

The approved policy deliberately preserves nested, singleton, and homogeneous collections instead of flattening them or encoding them as another type. This does not apply the SHOULD guidance in RFC 7946 section 3.1.8. Consumers that expect simplified forms may not handle or display these collections consistently; callers must prepare any simplification needed by their consumers. Antimeridian crossings also remain unchanged under section 3.1.9 as described above.

All six other supported types use their normal encoders when nested in a collection. A non-RFC member throws UnsupportedSpatialTypeException; an incompatible member structure throws UnsupportedGeometryStructureException. Failures propagate through every collection level: no member is omitted and no partial result is returned. JSON failures retain the shared JsonEncodingException contract and original exception.