///|
pub suberror TopoError {
Invalid(String)
} derive(Debug)
///|
pub struct Topology {
priv data : Json
} derive(Debug)
///|
fn field(j : Json, key : String) -> Json {
match j {
Object(m) => m.get(key).unwrap_or(Json::null())
_ => Json::null()
}
}
///|
fn array(j : Json) -> Array[Json] {
match j {
Array(a) => a
_ => []
}
}
///|
fn number(j : Json) -> Double {
match j {
Number(n, ..) => n
_ => 0.0
}
}
///|
fn kind(j : Json) -> String {
match field(j, "type") {
String(s) => s
_ => ""
}
}
///|
fn finite(n : Double) -> Bool {
!n.is_nan() && !n.is_inf()
}
///|
fn check_position(j : Json) -> Unit raise TopoError {
guard j is Array(a) else { raise Invalid("coordinate.array") }
if a.length() < 2 || a.length() > 8 {
raise Invalid("coordinate.dimension")
}
for v in a {
guard v is Number(n, ..) else { raise Invalid("coordinate.number") }
if !finite(n) || n.abs() > 1.0e12 {
raise Invalid("coordinate.range")
}
}
}
///|
fn check_refs(j : Json, depth : Int, count : Int) -> Unit raise TopoError {
guard j is Array(a) else { raise Invalid("arcs.array") }
for v in a {
if depth > 1 {
check_refs(v, depth - 1, count)
} else {
guard v is Number(n, ..) else { raise Invalid("arc.integer") }
if !finite(n) ||
n < -count.to_double() ||
n >= count.to_double() ||
n != n.to_int().to_double() {
raise Invalid("arc.index")
}
}
}
}
///|
fn check_geometry(g : Json, count : Int, depth : Int) -> Unit raise TopoError {
if depth > 16 {
raise Invalid("geometry.depth")
}
guard g is Object(_) else { raise Invalid("geometry.object") }
match field(g, "type") {
Null => ()
String("Point") => check_position(field(g, "coordinates"))
String("MultiPoint") => {
guard field(g, "coordinates") is Array(a) else {
raise Invalid("coordinates.array")
}
for p in a {
check_position(p)
}
}
String("LineString") => check_refs(field(g, "arcs"), 1, count)
String("MultiLineString") | String("Polygon") =>
check_refs(field(g, "arcs"), 2, count)
String("MultiPolygon") => check_refs(field(g, "arcs"), 3, count)
String("GeometryCollection") => {
guard field(g, "geometries") is Array(a) else {
raise Invalid("geometries.array")
}
for child in a {
check_geometry(child, count, depth + 1)
}
}
_ => raise Invalid("geometry.type")
}
}
///|
fn validate(data : Json) -> Unit raise TopoError {
if kind(data) != "Topology" {
raise Invalid("topology.type")
}
guard field(data, "arcs") is Array(arcs) else {
raise Invalid("topology.arcs")
}
if arcs.length() > 100000 {
raise Invalid("limit.arcs")
}
let mut points = 0
for arc in arcs {
guard arc is Array(ps) else { raise Invalid("arc.array") }
if ps.length() < 2 {
raise Invalid("arc.minimum")
}
points += ps.length()
if points > 1000000 {
raise Invalid("limit.points")
}
for p in ps {
check_position(p)
}
}
guard field(data, "objects") is Object(objects) else {
raise Invalid("topology.objects")
}
for _, g in objects {
check_geometry(g, arcs.length(), 0)
}
check_expansion(data)
let transform = field(data, "transform")
if transform != Json::null() {
for key in ["scale", "translate"] {
check_position(field(transform, key))
if array(field(transform, key)).length() != 2 {
raise Invalid("transform.dimension")
}
}
for s in array(field(transform, "scale")) {
if number(s) == 0.0 {
raise Invalid("transform.scale")
}
}
}
}
///|
/// Parse a TopoJSON document. Malformed shapes and invalid arc references raise TopoError.
pub fn read(text : String) -> Topology raise TopoError {
if text.length() > 16777216 {
raise Invalid("limit.text")
}
let mut depth = 0
let mut quoted = false
let mut escaped = false
for c in text.iter() {
if quoted {
if escaped {
escaped = false
} else if c == '\\' {
escaped = true
} else if c == '"' {
quoted = false
}
} else if c == '"' {
quoted = true
} else if c == '{' || c == '[' {
depth += 1
if depth > 32 {
raise Invalid("limit.depth")
}
} else if c == '}' || c == ']' {
depth -= 1
}
}
let data = @json.parse(text) catch { _ => raise Invalid("json.syntax") }
validate(data)
{ data, }
}
///|
pub fn Topology::write(self : Topology) -> String {
self.data.stringify()
}
///|
pub fn Topology::names(self : Topology) -> Array[String] {
let names = match field(self.data, "objects") {
Object(m) => m.keys().collect()
_ => []
}
names.sort()
names
}
///|
fn Topology::lookup(self : Topology, name : String) -> Json raise TopoError {
match field(self.data, "objects") {
Object(m) =>
match m.get(name) {
Some(g) => g
None => raise Invalid("object.missing:" + name)
}
_ => raise Invalid("topology.objects")
}
}
///|
/// Returns a detached topology geometry object (arc references are retained).
pub fn Topology::object(self : Topology, name : String) -> Json raise TopoError {
let g = self.lookup(name)
@json.parse(g.stringify()) catch {
_ => raise Invalid("json.copy")
}
}