---

title: "TopoJSON" author: "S. Wotherspoon" date: "16 January 2017" output: html_document


knitr::opts_chunk$set(echo = TRUE)

1. Introduction

TopoJSON is a JSON format for encoding geographic data structures into a shared topology. A TopoJSON topology represents one or geometries that share sequences of positions called arcs. TopoJSON, as an extension of GeoJSON, supports multiple geometry types: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, and GeometryCollection. Geometries in TopoJSON may contain additional properties to encode non-geometrical data.

1.1. Examples

A TopoJSON topology containing a single object named “example”, itself a geometry collection:

{
  "type": "Topology",
  "objects": {
    "example": {
      "type": "GeometryCollection",
      "geometries": [
        {
          "type": "Point",
          "properties": {
            "prop0": "value0"
          },
          "coordinates": [102, 0.5]
        },
        {
          "type": "LineString",
          "properties": {
            "prop0": "value0",
            "prop1": 0
          },
          "arcs": [0]
        },
        {
          "type": "Polygon",
          "properties": {
            "prop0": "value0",
            "prop1": {
              "this": "that"
            }
          },
          "arcs": [[-2]]
        }
      ]
    }
  },
  "arcs": [
    [[102, 0], [103, 1], [104, 0], [105, 1]],
    [[100, 0], [101, 0], [101, 1], [100, 1], [100, 0]]
  ]
}

The same topology, quantized:

{
  "type": "Topology",
  "transform": {
    "scale": [0.0005000500050005, 0.00010001000100010001],
    "translate": [100, 0]
  },
  "objects": {
    "example": {
      "type": "GeometryCollection",
      "geometries": [
        {
          "type": "Point",
          "properties": {
            "prop0": "value0"
          },
          "coordinates": [4000, 5000]
        },
        {
          "type": "LineString",
          "properties": {
            "prop0": "value0",
            "prop1": 0
          },
          "arcs": [0]
        },
        {
          "type": "Polygon",
          "properties": {
            "prop0": "value0",
            "prop1": {
              "this": "that"
            }
          },
          "arcs": [[1]]
        }
      ]
    }
  },
  "arcs": [
    [[4000, 0], [1999, 9999], [2000, -9999], [2000, 9999]],
    [[0, 0], [0, 9999], [2000, 0], [0, -9999], [-2000, 0]]
  ]
}

R

As R strings the two examples are

str1 <- "{
  \"type\": \"Topology\",
  \"objects\": {
    \"example\": {
      \"type\": \"GeometryCollection\",
      \"geometries\": [
        {
          \"type\": \"Point\",
          \"properties\": {
            \"prop0\": \"value0\"
          },
          \"coordinates\": [102, 0.5]
        },
        {
          \"type\": \"LineString\",
          \"properties\": {
            \"prop0\": \"value0\",
            \"prop1\": 0
          },
          \"arcs\": [0]
        },
        {
          \"type\": \"Polygon\",
          \"properties\": {
            \"prop0\": \"value0\",
            \"prop1\": {
              \"this\": \"that\"
            }
          },
          \"arcs\": [[-2]]
        }
      ]
    }
  },
  \"arcs\": [
    [[102, 0], [103, 1], [104, 0], [105, 1]],
    [[100, 0], [101, 0], [101, 1], [100, 1], [100, 0]]
  ]
}"

and

str2 <- "{
  \"type\": \"Topology\",
  \"transform\": {
    \"scale\": [0.0005000500050005, 0.00010001000100010001],
    \"translate\": [100, 0]
  },
  \"objects\": {
    \"example\": {
      \"type\": \"GeometryCollection\",
      \"geometries\": [
        {
          \"type\": \"Point\",
          \"properties\": {
            \"prop0\": \"value0\"
          },
          \"coordinates\": [4000, 5000]
        },
        {
          \"type\": \"LineString\",
          \"properties\": {
            \"prop0\": \"value0\",
            \"prop1\": 0
          },
          \"arcs\": [0]
        },
        {
          \"type\": \"Polygon\",
          \"properties\": {
            \"prop0\": \"value0\",
            \"prop1\": {
              \"this\": \"that\"
            }
          },
          \"arcs\": [[1]]
        }
      ]
    }
  },
  \"arcs\": [
    [[4000, 0], [1999, 9999], [2000, -9999], [2000, 9999]],
    [[0, 0], [0, 9999], [2000, 0], [0, -9999], [-2000, 0]]
  ]
}"

These can be converted to R lists with RJSON

library(rjson)
obj1 <- fromJSON(str1)
obj1
obj2 <- fromJSON(str2)
obj2

1.2. Definitions

JavaScript Object Notation (JSON), and the terms “object”, “name”, “value”, “array”, and “number”, are defined in IETF RTC 4627.

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in IETF RFC 2119.

All floating point numbers must be treated as double precision floats and all integers must be 32-bit signed integers.

2. TopoJSON Objects

TopoJSON always consists of a single topology object. A topology may contain any number of named geometry objects. The term “TopoJSON object” may refer to either a topology or a geometry object it contains.

mkGeometry <- function(obj,tr=NULL) {
  switch(obj$type,
         "Topology"=mkTopology(obj),
         "Point"=mkPoint(obj,tr),
         "MultiPoint"=mkMultiPoint(obj,tr),
         "LineString"=mkLineString(obj),
         "MultiLineString"=mkMultiLineString(obj),
         "Polygon"=mkPolygon(obj),
         "MultiPolygon"=mkMultiPolygon(obj),
         "GeometryCollection"=mkGeometryCollection(obj,tr))
}

2.1. Topology Objects

A topology is a TopoJSON object where the type member’s value is “Topology”.

mkTopology <- function(obj) {
  # Translate objects and arcs
  obj$objects <- lapply(obj$objects,mkGeometry,obj$tr)
  obj$arcs <- mkArcs(obj$arcs,obj$tr)
  class(obj) <- "Topology"
  obj
}
mkArcs <- function(arcs,tr=NULL) {
  lapply(arcs,function(arc) decodeArc(tr,do.call(rbind,arc)))
}

2.1.1. Positions

A position is represented by an array of numbers. There must be at least two elements, and may be more. The order of elements are recommended to follow x, y, z order (easting, northing, altitude for coordinates in a projected coordinate reference system, or longitude, latitude, altitude for coordinates in a geographic coordinate reference system). Any number of additional elements are allowed — interpretation and meaning of additional elements is beyond the scope of this specification.

2.1.2. Transforms

A topology may have a “transform” member whose value is a transform object. The purpose of the transform is to quantize positions for more efficient serialization, by representing positions as integers rather than floats.

Both the “scale” and “translate” members must be of length two. Every position in the topology must be quantized, with the first and second elements in each position an integer.

To transform from a quantized position to an absolute position:

  1. Multiply each quantized position element by the corresponding scale element.
  2. Add the corresponding translate element.

The following JavaScript reference implementation transforms a single position from the given quantized topology to absolute coordinates:

function transformPoint(topology, position) {
  position = position.slice();
  position[0] = position[0] * topology.transform.scale[0] + topology.transform.translate[0],
  position[1] = position[1] * topology.transform.scale[1] + topology.transform.translate[1]
  return position;
}

Note that by copying the input position, the reference implementation preserves any additional dimensions (such as z) without transforming them.

transformPoint <- function(tr,point) {
  if(!is.null(tr)) {
    point[1:2] <- point[1:2]*tr$scale+tr$translate
  }
  point
}
transformMultiPoint <- function(tr,multipoint) {
  if(!is.null(tr)) {
    multipoint[,1] <- multipoint[,1]*tr$scale[1]+tr$translate[1]
    multipoint[,2] <- multipoint[,2]*tr$scale[2]+tr$translate[2]
  }
  multipoint
}

2.1.3. Arcs

A topology must have an “arcs” member whose value is an array of arrays of positions. Each arc must be an array of two or more positions.

If a topology is quantized, the positions of each arc in the topology which are quantized must be delta-encoded. The first position of the arc is a normal position ([x_{1},y_{1}]). The second position ([x_{2}, y_{2}]) is encoded as ([\Delta x_{2}, \Delta y_{2}]), where (x_{2} = x_{1} + \Delta x_{2}) and(y_{2} = y_{1} + \Delta y_{2}). The third position ([x_{3}, y_{3}]) is encoded as ([\Delta x_{3}, \Delta y_{3}]), where (x_{3} = x_{2} + \Delta x_{3}) and(y_{3} = y_{2} + \Delta y_{3}) and so on.

The following JavaScript reference implementation decodes a single delta-encoded quantized arc from the given quantized topology:

function decodeArc(topology, arc) {
  var x = 0, y = 0;
  return arc.map(function(position) {
    position = position.slice();
    position[0] = (x += position[0]) * topology.transform.scale[0] + topology.transform.translate[0];
    position[1] = (y += position[1]) * topology.transform.scale[1] + topology.transform.translate[1];
    return position;
  });
}
decodeArc <- function(tr,arc) {
  if(!is.null(tr)) {
    arc[,1] <- cumsum(arc[,1])*tr$scale[1]+tr$translate[1]
    arc[,2] <- cumsum(arc[,2])*tr$scale[2]+tr$translate[2]
  }
  arc
}

2.1.4. Arc Indexes

A geometry object consisting of LineStrings (LineString or MultiLineString) or LinearRings (Polygon or MultiPolygon) must be constructed from arcs. Each arc must be referenced by numeric zero-based index into the containing topology’s arcs array. For example, 0 refers to the first arc, 1 refers to the second arc, and so on.

A negative arc index indicates that the arc at the ones’ complement of the index must be reversed to reconstruct the geometry: -1 refers to the reversed first arc, -2 refers to the reversed second arc, and so on. In JavaScript, you can negate a negative arc index i using the bitwise NOT operator, ~i.

If more than one arc is referenced to construct a LineString or LinearRing, the first position of a subsequent arc must be equal to the last position of the previous arc. Then, when reconstructing the geometry, the first position of each arc except the first may be dropped; equivalently, the last position of each arc except the last may be dropped.

2.1.5. Objects

A topology must have an “objects” member whose value is an object. This object may have any number of members, whose value must be a geometry object.

2.2. Geometry Objects

A geometry is a TopoJSON object where the type member’s value is one of the following strings: “Point”, “MultiPoint”, “LineString”, “MultiLineString”, “Polygon”, “MultiPolygon”, or “GeometryCollection”.

A TopoJSON geometry object of type “Point” or “MultiPoint” must have a member with the name “coordinates”. A TopoJSON geometry object of type “LineString”, “MultiLineString”, “Polygon”, or “MultiPolygon” must have a member with the name “arcs”. The value of the arcs and coordinates members is always an array. The structure for the elements in this array is determined by the type of geometry.

If a geometry has a commonly used identifier, that identifier should be included as a member of the geometry object with the name “id”. A geometry object may have a member with the name “properties”. The value of the properties member is an object (any JSON object or a JSON null value).

2.2.1. Point

For type “Point”, the “coordinates” member must be a single position.

mkPoint <- function(obj,tr) {
  obj$coordinates <- transformPoint(tr,obj$coordinates)
  class(obj) <- "Point"
  obj
}

2.2.2. MultiPoint

For type “MultiPoint”, the “coordinates” member must be an array of positions.

mkMultiPoint <- function(obj,tr) {
  ## Convert list to 2 column matrix
  obj$coordinates <- transformMultiPoint(tr,do.call(rbind,obj$coordinates))
  class(obj) <- "Point"
  obj
}

2.2.3. LineString

For type “LineString”, the “arcs” member must be an array of arc indexes.

mkLineString <- function(obj) {
  ## Change to 1-indexing - using reciporicals to detect negative 0.
  obj$arcs <- ifelse(1/obj$arcs < 0,abj$arcs-1,obj$arcs+1)
  class(obj) <- "LineString"
  obj
}

2.2.4. MultiLineString

For type “LineString”, the “arcs” member must be an array of LineString arc indexes.

mkLineString <- function(obj) {
  ## Change to 1-indexing - using reciporicals to detect negative 0.
  obj$arcs <- lapply(obj$arcs,function(arcs) ifelse(1/arcs < 0,arcs-1,arcs+1))
  class(obj) <- "MultiLineString"
  obj
}

2.2.5. Polygon

For type “Polygon”, the “arcs” member must be an array of LinearRing arc indexes. For Polygons with multiple rings, the first must be the exterior ring and any others must be interior rings or holes.

A LinearRing is closed LineString with 4 or more positions. The first and last positions are equivalent (they represent equivalent points).

mkPolygon <- function(obj) {
  ## Change to 1-indexing - using reciporicals to detect negative 0.
  obj$arcs <- lapply(obj$arcs,function(arcs) ifelse(1/arcs < 0,arcs-1,arcs+1))
  class(obj) <- "Polygon"
  obj
}

2.2.6. MultiPolygon

For type “MultiPolygon”, the “arcs” member must be an array of Polygon arc indexes.

mkPolygon <- function(obj) {
  ## Change to 1-indexing - using reciporicals to detect negative 0.
  obj$arcs <- lapply(obj$arcs,function(arr) lapply(arr,function(arcs) ifelse(1/arcs < 0,arcs-1,arcs+1)))
  class(obj) <- "MultiPolygon"
  obj
}

2.2.7. Geometry Collection

A TopoJSON object with type “GeometryCollection” is a geometry object which represents a collection of geometry objects.

A geometry collection must have a member with the name “geometries” whose value is an array. Each element in this array is a TopoJSON geometry object.

mkGeometryCollection <- function(obj,tr=NULL) {
  obj$geometries <- lapply(obj$geometries,mkGeometry,tr)
  class(obj) <- "GeometryCollection"
  obj
}

3. Bounding Boxes

To include information on the coordinate range for a topology or geometry a TopoJSON object may have a member named “bbox”. The value of the bbox member must be a (2 n) array where n is the number of dimensions represented in the contained geometries, with the lowest values for all axes followed by the highest values. The axes order of a bbox follows the axes order of geometries. The bounding box should not be transformed using the topology’s transform, if any.

Test

mkGeometry(obj1)
mkGeometry(obj2)


Try the silicate package in your browser

Any scripts or data that you put into this service are public.

silicate documentation built on Jan. 7, 2023, 1:15 a.m.