Thanks to visit codestin.com
Credit goes to github.com

Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ Version 0.9 (unreleased)
* Ensure that ``python setup.py clean`` removes all previously Cythonized and compiled
files (#239).
* Addition of a ``reverse`` function for GEOS >= 3.7 (#254).
* Addition of ``get_precision`` to get precision of a geometry and ``set_precision``
to set the precision of a geometry (may round and reduce coordinates).

Version 0.8 (2020-09-06)
------------------------
Expand Down
87 changes: 87 additions & 0 deletions pygeos/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"get_interior_ring",
"get_geometry",
"get_parts",
"get_precision",
"set_precision",
]


Expand Down Expand Up @@ -537,3 +539,88 @@ def get_num_geometries(geometry):
0
"""
return lib.get_num_geometries(geometry)


@requires_geos("3.6.0")
@multithreading_enabled
def get_precision(geometry):
"""Get the precision of a geometry.

If a precision has not been previously set, it will be 0 (double
precision). Otherwise, it will return the precision grid size that was
set on a geometry.

Returns NaN for not-a-geometry values.

Parameters
----------
geometry : Geometry or array_like

See also
--------
set_precision

Examples
--------
>>> get_precision(Geometry("POINT (1 1)"))
0.0
>>> geometry = set_precision(Geometry("POINT (1 1)"), 1.0)
>>> get_precision(geometry)
1.0
>>> np.isnan(get_precision(None))
True
"""
return lib.get_precision(geometry)


@requires_geos("3.6.0")
@multithreading_enabled
def set_precision(geometry, grid_size, preserve_topology=False):
"""Returns geometry with the precision set to a precision grid size.

By default, geometries use double precision coordinates (grid_size = 0).

Coordinates will be rounded if a precision grid is less precise than the
input geometry. Duplicated vertices will be dropped from lines and
polygons for grid sizes greater than 0. Line and polygon geometries may
collapse to empty geometries if all vertices are closer together than
grid_size. Z values, if present, will not be modified.

Note: subsequent operations will always be performed in the precision of
the geometry with higher precision (smaller "grid_size"). That same
precision will be attached to the operation outputs.

Also note: input geometries should be geometrically valid; unexpected
results may occur if input geometries are not.

Returns None if geometry is None.

Parameters
----------
geometry : Geometry or array_like
grid_size : float
Precision grid size. If 0, will use double precision (will not modify
geometry if precision grid size was not previously set). If this
value is more precise than input geometry, the input geometry will
not be modified.
preserve_topology : bool, optional (default: False)
If True, will attempt to preserve the topology of a geometry after
rounding coordinates.

See also
--------
get_precision

Examples
--------
>>> set_precision(Geometry("POINT (0.9 0.9)"), 1.0)
<pygeos.Geometry POINT (1 1)>
>>> set_precision(Geometry("POINT (0.9 0.9 0.9)"), 1.0)
<pygeos.Geometry POINT Z (1 1 0.9)>
>>> set_precision(Geometry("LINESTRING (0 0, 0 0.1, 0 1, 1 1)"), 1.0)
<pygeos.Geometry LINESTRING (0 0, 0 1, 1 1)>
>>> set_precision(None, 1.0) is None
True
"""

return lib.set_precision(geometry, grid_size, preserve_topology)
144 changes: 132 additions & 12 deletions pygeos/test/test_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,27 +339,147 @@ def test_get_parts_invalid_dimensions(geom):
pygeos.get_parts(geom)


@pytest.mark.parametrize(
"geom", [point, line_string, polygon],
)
@pytest.mark.parametrize("geom", [point, line_string, polygon])
def test_get_parts_non_multi(geom):
"""Non-multipart geometries should be returned identical to inputs"""
assert np.all(pygeos.equals_exact(np.asarray(geom), pygeos.get_parts(geom)))


@pytest.mark.parametrize(
"geom", [None, [None], []],
)
@pytest.mark.parametrize("geom", [None, [None], []])
def test_get_parts_None(geom):
assert len(pygeos.get_parts(geom)) == 0


@pytest.mark.parametrize(
"geom", ["foo", ["foo"], 42],
)
@pytest.mark.parametrize("geom", ["foo", ["foo"], 42])
def test_get_parts_invalid_geometry(geom):
with pytest.raises(
TypeError, match="One of the arguments is of incorrect type.",
):
with pytest.raises(TypeError, match="One of the arguments is of incorrect type."):
pygeos.get_parts(geom)


@pytest.mark.skipif(pygeos.geos_version < (3, 6, 0), reason="GEOS < 3.6")
def test_get_precision():
geometries = all_types + (point_z, empty_point, empty_line_string, empty_polygon)
# default is 0
actual = pygeos.get_precision(geometries).tolist()
assert actual == [0] * len(geometries)

geometry = pygeos.set_precision(geometries, 1)
actual = pygeos.get_precision(geometry).tolist()
assert actual == [1] * len(geometries)


@pytest.mark.skipif(pygeos.geos_version < (3, 6, 0), reason="GEOS < 3.6")
def test_get_precision_none():
assert np.all(np.isnan(pygeos.get_precision([None])))


@pytest.mark.skipif(pygeos.geos_version < (3, 6, 0), reason="GEOS < 3.6")
def test_set_precision():
initial_geometry = pygeos.Geometry("POINT (0.9 0.9)")
assert pygeos.get_precision(initial_geometry) == 0

geometry = pygeos.set_precision(initial_geometry, 0)
assert pygeos.get_precision(geometry) == 0
assert pygeos.equals(geometry, initial_geometry)

geometry = pygeos.set_precision(initial_geometry, 1)
assert pygeos.get_precision(geometry) == 1
assert pygeos.equals(geometry, pygeos.Geometry("POINT (1 1)"))
# original should remain unchanged
assert pygeos.equals(initial_geometry, pygeos.Geometry("POINT (0.9 0.9)"))


@pytest.mark.skipif(pygeos.geos_version < (3, 6, 0), reason="GEOS < 3.6")
def test_set_precision_drop_coords():
# setting precision of 0 will not drop duplicated points in original
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior does not match what is described in the docstring "If 0, will use floating point precision."

If floating point precision (1E-7 for single or 1E-14 for double) would have been used, the two equal points would be merged into one.

I think the docstring needs adjustment: 0 means, do not change the geometry.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is a bit more general than that; if you provide a more precise grid size than the grid size previously assigned to the geometry, the geometry should not be modified. This could happen if you first set a precision of 1, then set 0.1.

I'll try to make this more clear in the docstring.

geometry = pygeos.set_precision(
pygeos.Geometry("LINESTRING (0 0, 0 0, 0 1, 1 1)"), 0
)
assert pygeos.equals(geometry, pygeos.Geometry("LINESTRING (0 0, 0 0, 0 1, 1 1)"))

# setting precision will remove duplicated points
geometry = pygeos.set_precision(geometry, 1)
assert pygeos.equals(geometry, pygeos.Geometry("LINESTRING (0 0, 0 1, 1 1)"))


@pytest.mark.skipif(pygeos.geos_version < (3, 6, 0), reason="GEOS < 3.6")
def test_set_precision_z():
geometry = pygeos.set_precision(pygeos.Geometry("POINT Z (0.9 0.9 0.9)"), 1)
assert pygeos.get_precision(geometry) == 1
assert pygeos.equals(geometry, pygeos.Geometry("POINT Z (1 1 0.9)"))


@pytest.mark.skipif(pygeos.geos_version < (3, 6, 0), reason="GEOS < 3.6")
def test_set_precision_nan():
assert np.all(np.isnan(pygeos.get_coordinates(pygeos.set_precision(point_nan, 1))))


@pytest.mark.skipif(pygeos.geos_version < (3, 6, 0), reason="GEOS < 3.6")
def test_set_precision_none():
assert pygeos.set_precision(None, 0) is None


@pytest.mark.skipif(pygeos.geos_version < (3, 6, 0), reason="GEOS < 3.6")
def test_set_precision_grid_size_nan():
assert pygeos.set_precision(pygeos.Geometry("POINT (0.9 0.9)"), np.nan) is None


@pytest.mark.skipif(pygeos.geos_version < (3, 6, 0), reason="GEOS < 3.6")
def test_set_precision_preserve_topology():
# GEOS test case - geometry is valid initially but becomes
# invalid after rounding
geometry = pygeos.Geometry(
"POLYGON((10 10,20 10,16 15,20 20, 10 20, 14 15, 10 10))"
)

assert pygeos.equals(
pygeos.set_precision(geometry, 5, preserve_topology=False),
pygeos.Geometry("POLYGON ((10 10, 20 10, 15 15, 20 20, 10 20, 15 15, 10 10))"),
)

assert pygeos.equals(
pygeos.set_precision(geometry, 5, preserve_topology=True),
pygeos.Geometry(
"MULTIPOLYGON (((10 10, 15 15, 20 10, 10 10)), ((15 15, 10 20, 20 20, 15 15)))"
),
)


@pytest.mark.skipif(pygeos.geos_version < (3, 6, 0), reason="GEOS < 3.6")
@pytest.mark.parametrize(
"geometry,expected",
[
(
pygeos.Geometry("LINESTRING (0 0, 0.1 0.1)"),
pygeos.Geometry("LINESTRING EMPTY"),
),
(
pygeos.Geometry("LINEARRING (0 0, 0.1 0, 0.1 0.1, 0 0.1, 0 0)"),
pygeos.Geometry("LINEARRING EMPTY"),
),
(
pygeos.Geometry("POLYGON ((0 0, 0.1 0, 0.1 0.1, 0 0.1, 0 0))"),
pygeos.Geometry("POLYGON EMPTY"),
),
],
)
def test_set_precision_collapse(geometry, expected):
"""Lines and polygons collapse to empty geometries if vertices are too close"""
assert pygeos.equals(pygeos.set_precision(geometry, 1), expected)


@pytest.mark.skipif(pygeos.geos_version < (3, 6, 0), reason="GEOS < 3.6")
def test_set_precision_intersection():
"""Operations should use the most precise presision grid size of the inputs"""

box1 = pygeos.normalize(pygeos.box(0, 0, 0.9, 0.9))
box2 = pygeos.normalize(pygeos.box(0.75, 0, 1.75, 0.75))

assert pygeos.get_precision(pygeos.intersection(box1, box2)) == 0

# GEOS will use and keep the most precise precision grid size
box1 = pygeos.set_precision(box1, 0.5)
box2 = pygeos.set_precision(box2, 1)
out = pygeos.intersection(box1, box2)
assert pygeos.get_precision(out) == 0.5
assert pygeos.equals(out, pygeos.Geometry("LINESTRING (1 1, 1 0)"))
71 changes: 71 additions & 0 deletions src/ufuncs.c
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,19 @@ static void* get_z_data[1] = {GetZ};
#endif
static void* area_data[1] = {GEOSArea_r};
static void* length_data[1] = {GEOSLength_r};

#if GEOS_SINCE_3_6_0
static int GetPrecision(void* context, void* a, double* b) {
// GEOS returns -1 on error; 0 indicates double precision; > 0 indicates a precision
// grid size was set for this geometry.
double out = GEOSGeom_getPrecision_r(context, a);
if (out == -1) {
return 0;
}
*(double*)b = out;
return 1;
}
static void* get_precision_data[1] = {GetPrecision};
static int MinimumClearance(void* context, void* a, double* b) {
// GEOSMinimumClearance deviates from the pattern of returning 0 on exception and 1 on
// success for functions that return an int (it follows pattern for boolean functions
Expand Down Expand Up @@ -1609,6 +1621,63 @@ static void relate_pattern_func(char** args, npy_intp* dimensions, npy_intp* ste
}
static PyUFuncGenericFunction relate_pattern_funcs[1] = {&relate_pattern_func};

#if GEOS_SINCE_3_6_0
static char set_precision_dtypes[4] = {NPY_OBJECT, NPY_DOUBLE, NPY_BOOL, NPY_OBJECT};
static void set_precision_func(char** args, npy_intp* dimensions, npy_intp* steps,
void* data) {
GEOSGeometry* in1 = NULL;
GEOSGeometry** geom_arr;

CHECK_NO_INPLACE_OUTPUT(4);

// allocate a temporary array to store output GEOSGeometry objects
geom_arr = malloc(sizeof(void*) * dimensions[0]);
CHECK_ALLOC(geom_arr);

GEOS_INIT_THREADS;

QUATERNARY_LOOP {
// get the geometry: return on error
if (!get_geom(*(GeometryObject**)ip1, &in1)) {
errstate = PGERR_NOT_A_GEOMETRY;
destroy_geom_arr(ctx, geom_arr, i - 1);
break;
}
// grid size
double in2 = *(double*)ip2;
// preserve topology
npy_bool in3 = *(npy_bool*)ip3;
// flags:
// GEOS_PREC_NO_TOPO (1<<0): if set, do not try to preserve topology
// GEOS_PREC_KEEP_COLLAPSED (1<<1): Not used because uncollapsed geometries are
// invalid and will not be retained in GEOS >= 3.9 anyway.
int flags = in3 ? 0 : GEOS_PREC_NO_TOPO;

if ((in1 == NULL) | npy_isnan(in2)) {
// in case of a missing value: return NULL (None)
geom_arr[i] = NULL;
} else {
geom_arr[i] = GEOSGeom_setPrecision_r(ctx, in1, in2, flags);
if (geom_arr[i] == NULL) {
errstate = PGERR_GEOS_EXCEPTION;
destroy_geom_arr(ctx, geom_arr, i - 1);
break;
}
}
}

GEOS_FINISH_THREADS;

// fill the numpy array with PyObjects while holding the GIL
if (errstate == PGERR_SUCCESS) {
geom_arr_to_npy(geom_arr, args[3], steps[3], dimensions[0]);
}
free(geom_arr);
}

static PyUFuncGenericFunction set_precision_funcs[1] = {&set_precision_func};
#endif

/* define double -> geometry construction functions */
static char points_dtypes[2] = {NPY_DOUBLE, NPY_OBJECT};
static void points_func(char** args, npy_intp* dimensions, npy_intp* steps, void* data) {
Expand Down Expand Up @@ -2478,6 +2547,8 @@ int init_ufuncs(PyObject* m, PyObject* d) {

#if GEOS_SINCE_3_6_0
DEFINE_Y_d(minimum_clearance);
DEFINE_Y_d(get_precision);
DEFINE_CUSTOM(set_precision, 3);
#endif

#if GEOS_SINCE_3_7_0
Expand Down