Skip to content

Geospatial types

Chronicle recognizes a value as geospatial by its GeoJSON shape. The client ships three types that serialize to that shape, so the sink can index and query them geospatially. A hand-rolled coordinate class serializes as an ordinary nested object and gets none of that.

They live in io.cratis.chronicle.geospatial and are usable on any event, read model, or reducer state.

TypeConstructorGeoJSON typeSchema format
PointPoint(longitude, latitude)Pointpoint
LineStringLineString(coordinates)LineStringlinestring
PolygonPolygon(shell, holes)Polygonpolygon
LinearRingLinearRing(coordinates)(a polygon ring)
data class Point(val longitude: Double, val latitude: Double)
data class LineString(val coordinates: List<Point>)
data class LinearRing(val coordinates: List<Point>)
data class Polygon(
val shell: LinearRing,
val holes: List<LinearRing> = emptyList()
)

Longitude comes first, matching GeoJSON coordinate order — not the “latitude, longitude” order used in everyday speech.

LineString and LinearRing also take loose points (LineString(a, b, c)), which is what Java uses in place of a list literal. Polygon is @JvmOverloads, so new Polygon(shell) works from Java for a polygon with no holes.

import io.cratis.chronicle.events.EventType
import io.cratis.chronicle.geospatial.Point
@EventType
data class WarehouseInspected(
val warehouseId: String = "",
val inspectedAt: Point = Point(0.0, 0.0)
)
import io.cratis.chronicle.events.EventType;
import io.cratis.chronicle.geospatial.Point;
@EventType
public record WarehouseInspected(String warehouseId, Point inspectedAt) {}

Appending is unchanged — the client serializes the point as GeoJSON on the way out:

import io.cratis.chronicle.geospatial.Point
store.eventLog.append(
eventSourceId = "warehouse-1",
event = WarehouseInspected("warehouse-1", Point(10.75, 59.91))
)

The event content on the wire:

{
"warehouseId": "warehouse-1",
"inspectedAt": { "type": "Point", "coordinates": [10.75, 59.91] }
}

A polygon’s shell must be closed — its last point repeats its first — and needs at least four points. Holes follow the same rule.

import io.cratis.chronicle.geospatial.LinearRing
import io.cratis.chronicle.geospatial.Point
import io.cratis.chronicle.geospatial.Polygon
val deliveryZone = Polygon(
shell = LinearRing(
Point(10.70, 59.90),
Point(10.80, 59.90),
Point(10.80, 59.95),
Point(10.70, 59.90)
)
)

GeoJSON stores the shell and the holes in one flat array of rings, with the shell first. Reading a polygon back takes the first ring as the shell and the rest as holes.

Deserialization rejects geometries the sink would refuse, so a malformed value fails at the client rather than deep in the kernel.

GeometryRejected when
Pointcoordinates is not [longitude, latitude]
LineStringfewer than 2 points
Polygonit carries no rings at all
LinearRingfewer than 4 points, or it does not close
allthe declared type is not the expected one

Construction is not validated — only what comes off the wire is.

io.cratis.chronicle.json.ChronicleGson.chronicleGson is the Gson instance the client serializes event content and read model state with. Use it when you need to produce the same JSON the client would.

import io.cratis.chronicle.geospatial.Point
import io.cratis.chronicle.json.chronicleGson
val json = chronicleGson.toJson(Point(10.75, 59.91))
import io.cratis.chronicle.geospatial.Point;
import io.cratis.chronicle.json.ChronicleGson;
String json = ChronicleGson.chronicleGson.toJson(new Point(10.75, 59.91));