///|
fn json_field(value : Json, name : String) -> Json raise FgbError {
guard value is Object(fields) else {
raise InvalidArgument("expected JSON object")
}
match fields.get(name) {
Some(result) => result
None => raise InvalidArgument("missing JSON field \{name}")
}
}
///|
fn json_array(value : Json) -> Array[Json] raise FgbError {
guard value is Array(items) else {
raise InvalidArgument("expected JSON array")
}
items
}
///|
fn json_text(value : Json) -> String raise FgbError {
guard value is String(text) else {
raise InvalidArgument("expected JSON string")
}
text
}
///|
fn json_number(value : Json) -> Double raise FgbError {
guard value is Number(number, ..) else {
raise InvalidArgument("expected JSON number")
}
if !finite(number) {
raise InvalidArgument("non-finite JSON number")
}
number
}
///|
fn coordinate_json(coordinate : Coordinate) -> Json {
Json::array([Json::number(coordinate.x), Json::number(coordinate.y)])
}
///|
fn coordinates_json(
coordinates : Array[Coordinate],
start : Int32,
end : Int32,
) -> Json {
let items : Array[Json] = []
for i in start.. Json {
let items : Array[Json] = []
let mut start = 0
for end in part_ends(geometry) {
items.push(coordinates_json(geometry.xy, start, end))
start = end
}
Json::array(items)
}
///|
pub fn Geometry::to_geojson(self : Geometry) -> Json raise FgbError {
self.validate()
let coordinates = match self.kind {
Point => coordinate_json(self.xy[0])
LineString | MultiPoint => coordinates_json(self.xy, 0, self.xy.length())
Polygon | MultiLineString => parts_json(self)
MultiPolygon => Json::array(self.parts.map(part => parts_json(part)))
Unknown => raise InvalidArgument("cannot export Unknown geometry")
}
Json::object(
Map([("type", Json::string(self.kind.name())), ("coordinates", coordinates)]),
)
}
///|
fn parse_coordinate(value : Json) -> Coordinate raise FgbError {
let pair = json_array(value)
if pair.length() != 2 {
raise Unsupported("only 2D GeoJSON coordinate pairs are supported")
}
{ x: json_number(pair[0]), y: json_number(pair[1]), }
}
///|
fn parse_coordinates(
value : Json,
limits : Limits,
budget : Ref[Int32],
) -> Array[Coordinate] raise FgbError {
let values = json_array(value)
budget.val = checked_add(budget.val, values.length())
enforce(budget.val, limits.max_coordinates, "GeoJSON coordinates")
let result : Array[Coordinate] = []
for value in values {
result.push(parse_coordinate(value))
}
result
}
///|
fn parse_parts(
value : Json,
kind : GeometryKind,
limits : Limits,
budget : Ref[Int32],
) -> Geometry raise FgbError {
let parts = json_array(value)
enforce(parts.length(), limits.max_coordinates, "GeoJSON part count")
let xy : Array[Coordinate] = []
let ends : Array[Int32] = []
for part in parts {
let coordinates = parse_coordinates(part, limits, budget)
for coordinate in coordinates {
xy.push(coordinate)
}
ends.push(xy.length())
}
{ kind, xy, ends, parts: [], }
}
///|
pub fn geometry_from_geojson(
value : Json,
limits~ : Limits = Limits::default(),
) -> Geometry raise FgbError {
limits.validate()
let kind = json_text(json_field(value, "type"))
let coordinates = json_field(value, "coordinates")
let budget = Ref(0)
let geometry : Geometry = match kind {
"Point" =>
{ kind: Point, xy: [parse_coordinate(coordinates)], ends: [], parts: [], }
"LineString" =>
{
kind: LineString,
xy: parse_coordinates(coordinates, limits, budget),
ends: [],
parts: [],
}
"MultiPoint" =>
{
kind: MultiPoint,
xy: parse_coordinates(coordinates, limits, budget),
ends: [],
parts: [],
}
"Polygon" => parse_parts(coordinates, Polygon, limits, budget)
"MultiLineString" =>
parse_parts(coordinates, MultiLineString, limits, budget)
"MultiPolygon" => {
let values = json_array(coordinates)
enforce(values.length(), limits.max_coordinates, "GeoJSON polygon count")
let parts : Array[Geometry] = []
for polygon in values {
parts.push(parse_parts(polygon, Polygon, limits, budget))
}
{ kind: MultiPolygon, xy: [], ends: [], parts, }
}
_ => raise Unsupported("GeoJSON geometry \{kind}")
}
geometry.validate(limits~)
geometry
}
///|
pub fn Property::to_json_value(self : Property) -> Json {
match self {
Null => Json::null()
Boolean(b) => Json::boolean(b)
Integer(n) => Json::number(n.to_double(), repr=n.to_string())
Unsigned(n) => Json::number(n.to_double(), repr=n.to_string())
Real(n) => Json::number(n)
Text(text) => Json::string(text)
Binary(data) =>
Json::array(
Array::makei(data.length(), i => {
Json::number(data[i].to_int().to_double())
}),
)
}
}
///|
pub fn Feature::to_geojson(
self : Feature,
header : Header,
) -> Json raise FgbError {
if self.properties.length() != header.columns.length() {
raise InvalidArgument("feature property count differs from schema")
}
let properties : Map[String, Json] = Map([])
for i in 0..
@json.parse(text) catch {
_ => raise InvalidArgument("invalid JSON property")
}
Null => Json::null()
_ => raise InvalidArgument("JSON column must contain text or null")
}
} else {
self.properties[i].to_json_value()
}
properties[column.name] = value
}
Json::object(
Map([
("type", Json::string("Feature")),
(
"geometry",
match self.geometry {
None => Json::null()
Some(g) => g.to_geojson()
},
),
("properties", Json::object(properties)),
]),
)
}
///|
pub fn Dataset::to_geojson(
self : Dataset,
allow_projected~ : Bool = false,
) -> Json raise FgbError {
match self.header.crs {
Some(crs) =>
if !allow_projected &&
(
crs.code != 4326 ||
(crs.org != "" && crs.org != "EPSG" && crs.org != "epsg")
) {
raise InvalidArgument(
"GeoJSON export requires EPSG:4326 or explicit allow_projected",
)
}
None => ()
}
let features : Array[Json] = []
for feature in self.features {
features.push(feature.to_geojson(self.header))
}
Json::object(
Map([
("type", Json::string("FeatureCollection")),
("features", Json::array(features)),
]),
)
}
///|
fn property_from_json(
value : Json,
column : Column,
limits : Limits,
) -> Property raise FgbError {
if value == Json::null() {
if !column.nullable {
raise InvalidArgument("null non-nullable GeoJSON property \{column.name}")
}
return Null
}
let result : Property = match column.type_code {
0 | 3 | 5 | 7 => {
json_number(value) |> ignore
let n : Int64 = @json.from_json(Json::string(value.stringify())) catch {
_ => {
let number = json_number(value)
if number < -9007199254740991.0 ||
number > 9007199254740991.0 ||
number != @math.floor(number) {
raise InvalidArgument(
"expected exact signed integer for \{column.name}",
)
}
number.to_int64()
}
}
Integer(n)
}
1 | 4 | 6 | 8 => {
json_number(value) |> ignore
let n : UInt64 = @json.from_json(Json::string(value.stringify())) catch {
_ => {
let number = json_number(value)
if number < 0.0 ||
number > 9007199254740991.0 ||
number != @math.floor(number) {
raise InvalidArgument(
"expected exact unsigned integer for \{column.name}",
)
}
number.to_uint64()
}
}
Unsigned(n)
}
2 => {
guard value is True || value is False else {
raise InvalidArgument("expected boolean for \{column.name}")
}
Boolean(value == Json::boolean(true))
}
9 | 10 => Real(json_number(value))
11 | 13 => Text(json_text(value))
12 => Text(value.stringify())
14 => {
let array = json_array(value)
enforce(array.length(), limits.max_string_bytes, "GeoJSON binary bytes")
let bytes : Array[Byte] = []
for item in array {
let number = json_number(item)
if number < 0.0 ||
number > 255.0 ||
number != number.to_int().to_double() {
raise InvalidArgument("binary JSON array needs unsigned bytes")
}
bytes.push(number.to_int().to_byte())
}
Binary(Bytes::makei(bytes.length(), i => bytes[i]))
}
_ => raise Unsupported("GeoJSON property type \{column.type_code}")
}
let writer = ByteWriter::new()
write_property(writer, result, column.type_code, limits)
result
}
///|
pub fn feature_from_geojson(
value : Json,
header : Header,
limits~ : Limits = Limits::default(),
) -> Feature raise FgbError {
if json_text(json_field(value, "type")) != "Feature" {
raise InvalidArgument("GeoJSON item must be Feature")
}
let geometry_json = json_field(value, "geometry")
let geometry = if geometry_json == Json::null() {
None
} else {
Some(geometry_from_geojson(geometry_json, limits~))
}
match geometry {
Some(g) if header.geometry_kind != Unknown && header.geometry_kind != g.kind =>
raise InvalidArgument("GeoJSON geometry differs from schema")
_ => ()
}
let fields = match json_field(value, "properties") {
Object(fields) => fields
Null => Map([])
_ => raise InvalidArgument("GeoJSON properties must be an object or null")
}
let properties : Array[Property] = []
let names : Map[String, Bool] = Map([])
for column in header.columns {
names[column.name] = true
let value = fields.get(column.name).unwrap_or(Json::null())
properties.push(property_from_json(value, column, limits))
}
for name, _ in fields {
if !names.contains(name) {
raise InvalidArgument("GeoJSON property absent from schema: \{name}")
}
}
{ geometry, properties, }
}
///|
pub fn dataset_from_geojson(
value : Json,
header : Header,
limits~ : Limits = Limits::default(),
) -> Dataset raise FgbError {
limits.validate()
validate_header(header, limits~)
if json_text(json_field(value, "type")) != "FeatureCollection" {
raise InvalidArgument("expected GeoJSON FeatureCollection")
}
let values = json_array(json_field(value, "features"))
enforce(values.length(), limits.max_features, "GeoJSON features")
let features : Array[Feature] = []
for item in values {
features.push(feature_from_geojson(item, header, limits~))
}
{ header, features, }
}