Settings and tuning¶
By adopting the TopoJSON format is possible to store geographical data as topology. Adopting this approach makes smaller files than its GeoJSON counterpart. This process allows varies options, which are described below:
- topology
- prequantize
- topoquantize
- presimplify
- toposimplify
- shared_coords
- prevent_oversimplify
- simplify_with
- simplify_algorithm
- winding_order
- object_name
- ignore_index
topology¶
boolean
Specify if the topology should be computed for deriving the TopoJSON.
Default is True.
Example
Given the following two linestrings with one side sharing:
import topojson as tp
from shapely import geometry
data = geometry.MultiLineString([
[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]],
[[1, 0], [2, 0], [2, 1], [1, 1], [1, 0]]
])
data
By setting topology=False a TopoJSON structured file format is created without considering shared segments (the setting prequantize=False avoids computing the delta-encoding):
tp.Topology(data, topology=False, prequantize=False)
Topology(
{'arcs': [[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]],
[[1.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0], [1.0, 0.0]]],
'bbox': (0.0, 0.0, 2.0, 1.0),
'coordinates': [],
'objects': {'data': {'geometries': [{'arcs': [[0], [1]],
'id': 0,
'type': 'MultiLineString'}],
'type': 'GeometryCollection'}},
'type': 'Topology'}
)
'arcs': [[0], [1]]), where each segment is a single linestring (see: arcs).
When doing the same with topology=True, there are four arcs. The shared side is one arc, referenced by both lines: arc 3 by the second line and -4 by the first (arc -4 is arc 3 reversed).
tp.Topology(data, topology=True, prequantize=False)
Topology(
{'arcs': [[[0.0, 0.0], [1.0, 0.0]],
[[1.0, 1.0], [0.0, 1.0], [0.0, 0.0]],
[[1.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0]],
[[1.0, 1.0], [1.0, 0.0]]],
'bbox': (0.0, 0.0, 2.0, 1.0),
'coordinates': [],
'objects': {'data': {'geometries': [{'arcs': [[0, -4, 1], [2, 3]],
'id': 0,
'type': 'MultiLineString'}],
'type': 'GeometryCollection'}},
'type': 'Topology'}
)
prequantize¶
boolean, int, dict
If the prequantization parameter is specified, the input geometry is quantized prior to computing the topology. The returned topology is quantized, and its arcs are delta-encoded.
Quantization is recommended to
improve the quality of the topology if the input geometry is messy (i.e.,
small floating point error means that adjacent boundaries do not have
identical values); typical values are powers of ten, such as 1e4, 1e5 or
1e6. Default is True (which correspond to a quantize factor of 1e5).
Instead of a factor, a TopoJSON transform can be given as a dict,
{"scale": [kx, ky], "translate": [x0, y0]}. The grid is then fixed and does not
depend on the bounding box of the input, so topologies of different inputs (or of
different runs) share the same grid. This is what
incremental updates build on.
See quantization for how the grid follows the bounding box
and how to choose it yourself.
Example
Quantization is a two-step process, namely normalization and delta-encoding. Given the following two polygon with no sides shared, since the left-polygon has a x-max coordinate at 0.97 and the right-polygon has a x-min coordinate at 1.03:
import topojson as tp
from shapely import geometry
data = geometry.MultiLineString([
[[0, 0], [0.97, 0], [0.97, 1], [0, 1], [0, 0]],
[[1.03, 0], [2, 0], [2, 1], [1.03, 1], [1.03, 0]]
])
data
The prequantize option is defined as an integer number. It can be best understand as a value that defines the size of a rectangular grid, with the bottom left coordinate at (0,0). Next, the x-numbers and y-numbers of all coordinates are independency scaled and shifted on this rectangular grid (normalization on range). Here it is shown for the x-numbers only:
import numpy as np
# get the x-numbers of all coordinates
x = np.array([ls.xy[0] for ls in data.geoms])
print(f'x:\n{x}')
x:
[[0. 0.97 0.97 0. 0. ]
[1.03 2. 2. 1.03 1.03]]
# compute the scaling factor (kx) given the quantize factor (qf)
qf = 33
kx = (x.max() - x.min()) / (qf - 1)
print(f'kx: {kx}')
kx: 0.0625
# shift and apply the scaling factor to map the x-numbers on the integer range
xnorm = np.round((x - x.min()) / kx).astype(int)
print(f'x-normalized:\n{xnorm}')
x-normalized:
[[ 0 16 16 0 0]
[16 32 32 16 16]]
# denormalize happens as follow
print(f'x-denormalized:\n{xnorm * kx + x.min()}')
x-denormalized:
[[0. 1. 1. 0. 0.]
[1. 2. 2. 1. 1.]]
The delta-encoding is applied on the normalized coordinates and starting from the first coordinate, only the delta towards the following coordinate is stored. It is a character-reducing process, since the delta between two points is normally smaller than storing both coordinates. Here an example is shown for the x-numbers only (1D), where in real it is a 2D process:
# delta encoding of normalized x-numbers
x_quant = np.insert(np.diff(xnorm), 0, xnorm[:,0], axis=1)
print(f'x-quantized (normalized-delta-encoded):\n{x_quant}')
x-quantized (normalized-delta-encoded):
[[ 0 16 0 -16 0]
[ 16 16 0 -16 0]]
# dequantization of quantized x-numbers
x_dequant = x_quant.cumsum(axis=1) * kx + x.min()
print(f'x-dequantized:\n{x_dequant}')
x-dequantized:
[[0. 1. 1. 0. 0.]
[1. 2. 2. 1. 1.]]
So, to apply this prequantize value on the two no touching polygons, the polygons are touching as a result of it:
topo = tp.Topology(data, prequantize=33)
topo.to_svg()
Two polygons of a different size, quantized on the same fixed grid of 0.01:
grid = {"scale": [0.01, 0.01], "translate": [0, 0]}
small = tp.Topology(geometry.Polygon([[0, 0], [1, 0], [1, 1], [0, 1]]), prequantize=grid)
large = tp.Topology(geometry.Polygon([[0, 0], [2, 0], [2, 2], [0, 2]]), prequantize=grid)
small.to_dict()["arcs"], large.to_dict()["arcs"]
([[[0, 0], [0, 100], [100, 0], [0, -100], [-100, 0]]], [[[0, 0], [0, 200], [200, 0], [0, -200], [-200, 0]]])
{'scale': [0.01, 0.01], 'translate': [0.0, 0.0]}; with a
quantize factor each would get a grid of its own bounding box.
topoquantize¶
boolean or int
If the topoquantization parameter is specified, the input geometry is quantized
after the topology is constructed. If the topology is already quantized this
will be resolved first before the topoquantization is applied. See for more
details the prequantize parameter. Default is False.
See prequantize for an explained example.
Note: This is also supported by chaining. Meaning you could first compute the Topology (which can be cost-intensive) and afterwards apply the topoquantize on the computed Topology.
Example
import topojson as tp
data = tp.utils.example_data_africa()
topo = tp.Topology(data)
topo_tq = topo.topoquantize(75)
print(f'length with topoquantization: {len(topo_tq.to_json())}')
print(f'length without topoquantization: {len(topo.to_json())}')
length with topoquantization: 20099
length without topoquantization: 29513
presimplify¶
boolean, float
Apply presimplify to remove unnecessary points from linestrings before the
topology is constructed. This will simplify the input geometries. True uses a
tolerance of 2. Default is False.
The lines are simplified one by one, so the two sides of a shared border can be
simplified apart. For polygons, simplify_with="geos" simplifies them together as
a coverage instead, see simplify_with.
Example
Here we load continental Africa as data file and apply the presimplify on the input-space.
The plot shows the borders including linestring simplification, derived before the Topology is computed.
import topojson as tp
data = tp.utils.example_data_africa()
topo = tp.Topology(data, presimplify=4)
# since SVG rendering is too small for this example,
# we use the (optional!) package Altair for visualization.
topo.to_alt().properties(title='presimplify')
toposimplify¶
boolean, float
Apply toposimplify to remove unnecessary points from arcs after the topology
is constructed. This will simplify the constructed arcs without altering the
topological relations. Sensible values for coordinates stored in degrees are
in the range of 0.0001 to 10. Defaults to False.
Note 1: The units of toposimplify are corresponding to the input space. The provided sensible values are for degrees (eg. epsg:4326). When the projection of your data is in meters you might need to test which value should be adopted.
Note 2: This is also supported by chaining. Meaning you could first compute the Topology (which can be cost-intensive) and afterwards apply the toposimplify on the computed Topology.
Note 3: With prevent_oversimplify (the default), a ring that would be reduced to fewer than three points keeps the vertices to stay a triangle, so small islands do not collapse.
Example
Here we load continental Africa as data file and apply the toposimplify on the arcs.
The plot shows the borders including linestring simplification, derived after the Topology is computed.
import topojson as tp
data = tp.utils.example_data_africa()
topo = tp.Topology(data, toposimplify=4)
# since SVG rendering is too small for this example,
# we use the (optional!) package Altair for visualization.
topo.to_alt().properties(title='toposimplify')
Instead of a tolerance, keep gives the share of the vertices to keep, between 0 and
1. Each inner vertex of an arc gets a weight: the largest epsilon at which
Douglas-Peucker still keeps it. keep keeps the vertices with the largest weights,
and the ends of the arcs always, so the result is the same as with the matching
epsilon. As the arcs are shared, shared borders stay matched.
With simplify_algorithm="vw", keep searches the tolerance of Visvalingam-Whyatt
(package simplification) that leaves the share asked for. VW is nested, so each step
of the search continues from the result of the step before, and the result is again
that of the matching epsilon.
Example
A line of 9 inner vertices (blue) with the weight of each vertex, simplified to a quarter and to half of its inner vertices (dashed); the kept vertices are filled.
topo = tp.Topology(data)
topo.toposimplify(keep=0.1) # keep 10% of the vertices
shared_coords¶
boolean
Sets the strategy to detect junctions. When set to False a path is considered
shared when coordinates are the same path (path-connected). The path-connected
strategy is more 'correct', but slightly slower. When set to True a path is
considered shared when all coordinates appear in both paths
(coords-connected). Default is False.
Example
The example shows two LineStrings, the are partially overlapping with one shared coordinate.
import topojson as tp
from shapely import geometry
data = geometry.MultiLineString([
[(0, 0), (10, 0), (10, 5), (20, 5)],
[(5, 0), (20, 0), (20, 5), (10, 5), (0, 5)]
])
data
The setting shared_coords=True adopts a strategy of coords-connected, meaning it will split only when coordinates are detected in both linestrings. Here we show it as SVG, with each derived segment/arc plotted separately.
tp.Topology(
data,
shared_coords=True,
prequantize=False
).to_svg(separate=True)
When using the setting shared_coords=False a path-connected strategy is adopted, meaning it will split also when paths are overlapping without having common coordinates.
tp.Topology(
data,
shared_coords=False,
prequantize=False
).to_svg(separate=True)
prevent_oversimplify¶
boolean
If this setting is set to True, the simplification is slower, but the
likelihood of producing valid geometries is higher as it prevents
oversimplification. Simplification happens on paths separately, so this
setting is especially relevant for rings with no partial shared paths. This
is also known as a topology-preserving variant of simplification.
Default is True.
Example
The example shows a circle that is two times simplified. The first time with prevent_oversimplify=False, where the circle collapses to a single point, and the second time with prevent_oversimplify=True, where it keeps four points.
import topojson as tp
from shapely import geometry
circle = geometry.Point(0, 0).buffer(1)
circle
# force simplification
tp.Topology(
circle,
toposimplify=2,
prevent_oversimplify=False
).to_svg()
# avoid oversimplification
tp.Topology(
circle,
toposimplify=2,
prevent_oversimplify=True
).to_svg()
simplify_with¶
str
Sets the package to use for simplifying (both pre- and toposimplify). Choose
between shapely, simplification or geos. Shapely adopts solely Douglas-Peucker
and simplification both Douglas-Peucker and Visvalingam-Whyatt. The package
simplification is known to be quicker than shapely.
Default is shapely.
geos applies to presimplify only. It simplifies the polygons together as a
coverage (shapely.coverage_simplify, Visvalingam-Whyatt): a shared border is
simplified once, so the neighbours stay matched, and each ring stays at least a
triangle. Other lines are simplified with shapely. The polygons should form a valid
coverage: no overlaps, and the vertices of shared borders equal
(shapely.coverage_is_valid). Toposimplify simplifies arcs, to which this does not
apply.
Example
import topojson as tp
data = tp.utils.example_data_africa()
topo = tp.Topology(data, prequantize=200)
# we visualize with the (optional!) package Altair,
# since SVG rendering is too small for this example
topo.toposimplify(
epsilon=1,
simplify_algorithm='dp',
simplify_with='simplification',
prevent_oversimplify=False
).to_alt().properties(title='Douglas-Peucker (package simplification)')
With presimplify, shapely simplifies each line on its own, so neighbours drift
apart (the double lines on the left); with geos the polygons are simplified as a
coverage:
presimplified = tp.Topology(data, presimplify=1).to_alt()
coverage = tp.Topology(data, presimplify=1, simplify_with="geos").to_alt()
presimplified | coverage
simplify_algorithm¶
str
Choose between dp and vw, for Douglas-Peucker or Visvalingam-Whyatt
respectively. vw will only be selected if simplify_with is set to
simplification. Default is dp, since it still "produces the most accurate
generalization" (Shi & Cheung, 2006).
Example
Here we use the vw algorithm from Visvalingam-Whyatt included in the package
simplification.
import topojson as tp
data = tp.utils.example_data_africa()
topo = tp.Topology(data, prequantize=200)
# we visualize with the (optional!) package Altair,
# since SVG rendering is too small for this example
topo.toposimplify(
epsilon=1,
simplify_algorithm='vw',
simplify_with='simplification',
prevent_oversimplify=True
).to_alt().properties(title='Visvalingam-Whyatt (package simplification)')
winding_order¶
str
Determines the winding order of the features in the output geometry. Choose
between CW_CCW for clockwise orientation for outer rings and counter-
clockwise for interior rings. Or CCW_CW for counter-clockwise for outer
rings and clockwise for interior rings. Default is CW_CCW.
Example
Start with creating a Polygon geometry
import topojson as tp
from shapely import geometry
data = geometry.shape({
"type": "Polygon",
"coordinates": [[[0,0],[10,0],[10,10],[0,10],[0,0]]]
})
data
Continue with defining two Topology objects, one with the outer rings clock wise and the inner rings counter clockwise (CW_CCW) and one with the outer rings counter clock wise and the inner rings clock wise (CCW_CW).
CW_CCW = tp.Topology(data, winding_order='CW_CCW', prequantize=False)
CCW_CW = tp.Topology(data, winding_order='CCW_CW', prequantize=False)
print(CW_CCW)
print(CCW_CW)
Topology(
{'arcs': [[[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0], [0.0, 0.0]]],
'bbox': (0.0, 0.0, 10.0, 10.0),
'coordinates': [],
'objects': {'data': {'geometries': [{'arcs': [[0]],
'id': 0,
'type': 'Polygon'}],
'type': 'GeometryCollection'}},
'type': 'Topology'}
)
Topology(
{'arcs': [[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]]],
'bbox': (0.0, 0.0, 10.0, 10.0),
'coordinates': [],
'objects': {'data': {'geometries': [{'arcs': [[0]],
'id': 0,
'type': 'Polygon'}],
'type': 'GeometryCollection'}},
'type': 'Topology'}
)
arcs for type Polygon are reversed. The effect seems to be negligible, but the effect should be taken into account when using geographic projections, as it defines which part is 'inside' and 'outside' the Polygon:
alt_top = CW_CCW.to_alt(projection='equalEarth', color='type:N').properties(title='CW_CCW')
alt_bottom = CCW_CW.to_alt(projection='equalEarth', color='type:N').properties(title='CCW_CW')
alt_top & alt_bottom
object_name¶
str or list of str
Name of the object in the TopoJSON. A list of GeoDataFrames can be given as data,
with a list of names of the same length: each GeoDataFrame then becomes an object
of its own, with the topology computed across all of them.
Default is data.
Example
import geopandas
import topojson as tp
from shapely import geometry
left = geopandas.GeoDataFrame({"name": ["a"]}, geometry=[geometry.box(0, 0, 1, 1)])
right = geopandas.GeoDataFrame({"name": ["b"]}, geometry=[geometry.box(1, 0, 2, 1)])
topo = tp.Topology([left, right], object_name=["left", "right"])
list(topo.to_dict()["objects"])
['left', 'right']
ignore_index¶
boolean
If True, the ids of the features of a GeoJSON FeatureCollection are ignored and
replaced by new ones. Otherwise features keep their id, and a duplicate id raises
an error.
Default is False.