Fixed #24152 -- Deprecated GeoQuerySet aggregate methods

Thanks Josh Smeaton and Tim Graham for the reviews.
This commit is contained in:
Claude Paroz 2015-01-14 20:48:55 +01:00
parent 5338ff4808
commit a79e6b6717
10 changed files with 208 additions and 75 deletions

View File

@ -1,5 +1,5 @@
from django.db.models.aggregates import Aggregate
from django.contrib.gis.db.models.fields import GeometryField, ExtentField
from django.contrib.gis.db.models.fields import ExtentField
__all__ = ['Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union']
@ -20,9 +20,9 @@ class GeoAggregate(Aggregate):
self.template = '%(function)s(SDOAGGRTYPE(%(expressions)s,%(tolerance)s))'
return self.as_sql(compiler, connection)
def prepare(self, query=None, allow_joins=True, reuse=None, summarize=False):
c = super(GeoAggregate, self).prepare(query, allow_joins, reuse, summarize)
if not isinstance(self.expressions[0].output_field, GeometryField):
def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
c = super(GeoAggregate, self).resolve_expression(query, allow_joins, reuse, summarize, for_save)
if not hasattr(c.input_field.field, 'geom_type'):
raise ValueError('Geospatial aggregates only allowed on geometry fields.')
return c

View File

@ -1,3 +1,5 @@
import warnings
from django.db import connections
from django.db.models.expressions import RawSQL
from django.db.models.fields import Field
@ -15,6 +17,7 @@ from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Area, Distance
from django.utils import six
from django.utils.deprecation import RemovedInDjango20Warning
class GeoQuerySet(QuerySet):
@ -65,6 +68,11 @@ class GeoQuerySet(QuerySet):
This is analogous to a union operation, but much faster because
boundaries are not dissolved.
"""
warnings.warn(
"The collect GeoQuerySet method is deprecated. Use the Collect() "
"aggregate in an aggregate() or annotate() method.",
RemovedInDjango20Warning, stacklevel=2
)
return self._spatial_aggregate(aggregates.Collect, **kwargs)
def difference(self, geom, **kwargs):
@ -105,6 +113,11 @@ class GeoQuerySet(QuerySet):
Returns the extent (aggregate) of the features in the GeoQuerySet. The
extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax).
"""
warnings.warn(
"The extent GeoQuerySet method is deprecated. Use the Extent() "
"aggregate in an aggregate() or annotate() method.",
RemovedInDjango20Warning, stacklevel=2
)
return self._spatial_aggregate(aggregates.Extent, **kwargs)
def extent3d(self, **kwargs):
@ -113,6 +126,11 @@ class GeoQuerySet(QuerySet):
GeoQuerySet. It is returned as a 6-tuple, comprising:
(xmin, ymin, zmin, xmax, ymax, zmax).
"""
warnings.warn(
"The extent3d GeoQuerySet method is deprecated. Use the Extent3D() "
"aggregate in an aggregate() or annotate() method.",
RemovedInDjango20Warning, stacklevel=2
)
return self._spatial_aggregate(aggregates.Extent3D, **kwargs)
def force_rhr(self, **kwargs):
@ -215,6 +233,11 @@ class GeoQuerySet(QuerySet):
this GeoQuerySet and returns it. This is a spatial aggregate
method, and thus returns a geometry rather than a GeoQuerySet.
"""
warnings.warn(
"The make_line GeoQuerySet method is deprecated. Use the MakeLine() "
"aggregate in an aggregate() or annotate() method.",
RemovedInDjango20Warning, stacklevel=2
)
return self._spatial_aggregate(aggregates.MakeLine, geo_field_type=PointField, **kwargs)
def mem_size(self, **kwargs):
@ -398,6 +421,11 @@ class GeoQuerySet(QuerySet):
None if the GeoQuerySet is empty. The `tolerance` keyword is for
Oracle backends only.
"""
warnings.warn(
"The unionagg GeoQuerySet method is deprecated. Use the Union() "
"aggregate in an aggregate() or annotate() method.",
RemovedInDjango20Warning, stacklevel=2
)
return self._spatial_aggregate(aggregates.Union, **kwargs)
### Private API -- Abstracted DRY routines. ###

View File

@ -6,7 +6,8 @@ from unittest import skipUnless
from django.contrib.gis.gdal import HAS_GDAL
from django.contrib.gis.geos import HAS_GEOS
from django.test import TestCase, skipUnlessDBFeature
from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils._os import upath
if HAS_GEOS:
@ -206,6 +207,7 @@ class Geo3DTest(TestCase):
# Ordering of points in the resulting geometry may vary between implementations
self.assertSetEqual({p.ewkt for p in ref_union}, {p.ewkt for p in union})
@ignore_warnings(category=RemovedInDjango20Warning)
def test_extent(self):
"""
Testing the Extent3D aggregate for 3D models.
@ -223,6 +225,7 @@ class Geo3DTest(TestCase):
for e3d in [extent1, extent2]:
check_extent3d(e3d)
self.assertIsNone(City3D.objects.none().extent3d())
self.assertIsNone(City3D.objects.none().aggregate(Extent3D('point'))['point__extent3d'])
def test_perimeter(self):
"""

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from datetime import datetime
from django.contrib.gis.db.models import Extent
from django.contrib.gis.geos import HAS_GEOS
from django.contrib.gis.shortcuts import render_to_kmz
from django.contrib.gis.tests.utils import no_oracle
@ -44,7 +45,7 @@ class GeoRegressionTests(TestCase):
"Testing `extent` on a table with a single point. See #11827."
pnt = City.objects.get(name='Pueblo').point
ref_ext = (pnt.x, pnt.y, pnt.x, pnt.y)
extent = City.objects.filter(name='Pueblo').extent()
extent = City.objects.filter(name='Pueblo').aggregate(Extent('point'))['point__extent']
for ref_val, val in zip(ref_ext, extent):
self.assertAlmostEqual(ref_val, val, 4)

View File

@ -5,11 +5,13 @@ from tempfile import NamedTemporaryFile
from django.db import connection
from django.contrib.gis import gdal
from django.contrib.gis.db.models import Extent, MakeLine, Union
from django.contrib.gis.geos import HAS_GEOS
from django.contrib.gis.tests.utils import no_oracle, oracle, postgis, spatialite
from django.core.management import call_command
from django.test import TestCase, skipUnlessDBFeature
from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
from django.utils import six
from django.utils.deprecation import RemovedInDjango20Warning
if HAS_GEOS:
from django.contrib.gis.geos import (fromstr, GEOSGeometry,
@ -470,19 +472,26 @@ class GeoQuerySetTest(TestCase):
self.assertIsInstance(country.envelope, Polygon)
@skipUnlessDBFeature("supports_extent_aggr")
@ignore_warnings(category=RemovedInDjango20Warning)
def test_extent(self):
"Testing the `extent` GeoQuerySet method."
"""
Testing the (deprecated) `extent` GeoQuerySet method and the Extent
aggregate.
"""
# Reference query:
# `SELECT ST_extent(point) FROM geoapp_city WHERE (name='Houston' or name='Dallas');`
# => BOX(-96.8016128540039 29.7633724212646,-95.3631439208984 32.7820587158203)
expected = (-96.8016128540039, 29.7633724212646, -95.3631439208984, 32.782058715820)
qs = City.objects.filter(name__in=('Houston', 'Dallas'))
extent = qs.extent()
extent1 = qs.extent()
extent2 = qs.aggregate(Extent('point'))['point__extent']
for val, exp in zip(extent, expected):
self.assertAlmostEqual(exp, val, 4)
for extent in (extent1, extent2):
for val, exp in zip(extent, expected):
self.assertAlmostEqual(exp, val, 4)
self.assertIsNone(City.objects.filter(name=('Smalltown')).extent())
self.assertIsNone(City.objects.filter(name=('Smalltown')).aggregate(Extent('point'))['point__extent'])
@skipUnlessDBFeature("has_force_rhr_method")
def test_force_rhr(self):
@ -614,11 +623,17 @@ class GeoQuerySetTest(TestCase):
# Only PostGIS has support for the MakeLine aggregate.
@skipUnlessDBFeature("supports_make_line_aggr")
@ignore_warnings(category=RemovedInDjango20Warning)
def test_make_line(self):
"Testing the `make_line` GeoQuerySet method."
"""
Testing the (deprecated) `make_line` GeoQuerySet method and the MakeLine
aggregate.
"""
# Ensuring that a `TypeError` is raised on models without PointFields.
self.assertRaises(TypeError, State.objects.make_line)
self.assertRaises(TypeError, Country.objects.make_line)
# MakeLine on an inappropriate field returns simply None
self.assertIsNone(State.objects.aggregate(MakeLine('poly'))['poly__makeline'])
# Reference query:
# SELECT AsText(ST_MakeLine(geoapp_city.point)) FROM geoapp_city;
ref_line = GEOSGeometry(
@ -629,9 +644,11 @@ class GeoQuerySetTest(TestCase):
)
# We check for equality with a tolerance of 10e-5 which is a lower bound
# of the precisions of ref_line coordinates
line = City.objects.make_line()
self.assertTrue(ref_line.equals_exact(line, tolerance=10e-5),
"%s != %s" % (ref_line, line))
line1 = City.objects.make_line()
line2 = City.objects.aggregate(MakeLine('point'))['point__makeline']
for line in (line1, line2):
self.assertTrue(ref_line.equals_exact(line, tolerance=10e-5),
"%s != %s" % (ref_line, line))
@skipUnlessDBFeature("has_num_geom_method")
def test_num_geom(self):
@ -813,24 +830,34 @@ class GeoQuerySetTest(TestCase):
# but this seems unexpected and should be investigated to determine the cause.
@skipUnlessDBFeature("has_unionagg_method")
@no_oracle
@ignore_warnings(category=RemovedInDjango20Warning)
def test_unionagg(self):
"Testing the `unionagg` (aggregate union) GeoQuerySet method."
"""
Testing the (deprecated) `unionagg` (aggregate union) GeoQuerySet method
and the Union aggregate.
"""
tx = Country.objects.get(name='Texas').mpoly
# Houston, Dallas -- Ordering may differ depending on backend or GEOS version.
union1 = fromstr('MULTIPOINT(-96.801611 32.782057,-95.363151 29.763374)')
union2 = fromstr('MULTIPOINT(-95.363151 29.763374,-96.801611 32.782057)')
qs = City.objects.filter(point__within=tx)
self.assertRaises(TypeError, qs.unionagg, 'name')
self.assertRaises(ValueError, qs.aggregate, Union('name'))
# Using `field_name` keyword argument in one query and specifying an
# order in the other (which should not be used because this is
# an aggregate method on a spatial column)
u1 = qs.unionagg(field_name='point')
u2 = qs.order_by('name').unionagg()
u3 = qs.aggregate(Union('point'))['point__union']
u4 = qs.order_by('name').aggregate(Union('point'))['point__union']
tol = 0.00001
self.assertTrue(union1.equals_exact(u1, tol) or union2.equals_exact(u1, tol))
self.assertTrue(union1.equals_exact(u2, tol) or union2.equals_exact(u2, tol))
self.assertTrue(union1.equals_exact(u3, tol) or union2.equals_exact(u3, tol))
self.assertTrue(union1.equals_exact(u4, tol) or union2.equals_exact(u4, tol))
qs = City.objects.filter(name='NotACity')
self.assertIsNone(qs.unionagg(field_name='point'))
self.assertIsNone(qs.aggregate(Union('point'))['point__union'])
def test_non_concrete_field(self):
NonConcreteModel.objects.create(point=Point(0, 0), name='name')

View File

@ -3,9 +3,10 @@ from __future__ import unicode_literals
from django.contrib.gis.geos import HAS_GEOS
from django.contrib.gis.tests.utils import no_oracle
from django.db import connection
from django.test import TestCase, skipUnlessDBFeature
from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
from django.test.utils import override_settings
from django.utils import timezone
from django.utils.deprecation import RemovedInDjango20Warning
if HAS_GEOS:
from django.contrib.gis.db.models import Collect, Count, Extent, F, Union
@ -64,7 +65,8 @@ class RelatedGeoModelTest(TestCase):
check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point)
@skipUnlessDBFeature("supports_extent_aggr")
def test04a_related_extent_aggregate(self):
@ignore_warnings(category=RemovedInDjango20Warning)
def test_related_extent_aggregate(self):
"Testing the `extent` GeoQuerySet aggregates on related geographic models."
# This combines the Extent and Union aggregates into one query
aggs = City.objects.aggregate(Extent('location__point'))
@ -83,8 +85,22 @@ class RelatedGeoModelTest(TestCase):
for ref_val, e_val in zip(ref, e):
self.assertAlmostEqual(ref_val, e_val, tol)
@skipUnlessDBFeature("supports_extent_aggr")
def test_related_extent_annotate(self):
"""
Test annotation with Extent GeoAggregate.
"""
cities = City.objects.annotate(points_extent=Extent('location__point')).order_by('name')
tol = 4
self.assertAlmostEqual(
cities[0].points_extent,
(-97.516111, 33.058333, -97.516111, 33.058333),
tol
)
@skipUnlessDBFeature("has_unionagg_method")
def test04b_related_union_aggregate(self):
@ignore_warnings(category=RemovedInDjango20Warning)
def test_related_union_aggregate(self):
"Testing the `unionagg` GeoQuerySet aggregates on related geographic models."
# This combines the Extent and Union aggregates into one query
aggs = City.objects.aggregate(Union('location__point'))
@ -277,8 +293,12 @@ class RelatedGeoModelTest(TestCase):
self.assertEqual(None, b.author)
@skipUnlessDBFeature("supports_collect_aggr")
def test14_collect(self):
"Testing the `collect` GeoQuerySet method and `Collect` aggregate."
@ignore_warnings(category=RemovedInDjango20Warning)
def test_collect(self):
"""
Testing the (deprecated) `collect` GeoQuerySet method and `Collect`
aggregate.
"""
# Reference query:
# SELECT AsText(ST_Collect("relatedapp_location"."point")) FROM "relatedapp_city" LEFT OUTER JOIN
# "relatedapp_location" ON ("relatedapp_city"."location_id" = "relatedapp_location"."id")

View File

@ -161,6 +161,9 @@ details on these changes.
* Support for the legacy ``%(<foo>)s`` syntax in ``ModelFormMixin.success_url``
will be removed.
* ``GeoQuerySet`` aggregate methods ``collect()``, ``extent()``, ``extent3d()``,
``makeline()``, and ``union()`` will be removed.
.. _deprecation-removed-in-1.9:
1.9

View File

@ -268,12 +268,9 @@ Method PostGIS Oracle SpatiaLite
==================================== ======= ====== ==========
:meth:`GeoQuerySet.area` X X X
:meth:`GeoQuerySet.centroid` X X X
:meth:`GeoQuerySet.collect` X (from v3.0)
:meth:`GeoQuerySet.difference` X X X
:meth:`GeoQuerySet.distance` X X X
:meth:`GeoQuerySet.envelope` X X
:meth:`GeoQuerySet.extent` X X (from v3.0)
:meth:`GeoQuerySet.extent3d` X
:meth:`GeoQuerySet.force_rhr` X
:meth:`GeoQuerySet.geohash` X
:meth:`GeoQuerySet.geojson` X X
@ -281,7 +278,6 @@ Method PostGIS Oracle SpatiaLite
:meth:`GeoQuerySet.intersection` X X X
:meth:`GeoQuerySet.kml` X X
:meth:`GeoQuerySet.length` X X X
:meth:`GeoQuerySet.make_line` X
:meth:`GeoQuerySet.mem_size` X
:meth:`GeoQuerySet.num_geom` X X X
:meth:`GeoQuerySet.num_points` X X X
@ -295,7 +291,23 @@ Method PostGIS Oracle SpatiaLite
:meth:`GeoQuerySet.transform` X X X
:meth:`GeoQuerySet.translate` X X
:meth:`GeoQuerySet.union` X X X
:meth:`GeoQuerySet.unionagg` X X X
==================================== ======= ====== ==========
Aggregate Functions
-------------------
The following table provides a summary of what GIS-specific aggregate functions
are available on each spatial backend. Please note that MySQL does not
support any of these aggregates, and is thus excluded from the table.
==================================== ======= ====== ==========
Aggregate PostGIS Oracle SpatiaLite
==================================== ======= ====== ==========
:class:`Collect` X (from v3.0)
:class:`Extent` X X (from v3.0)
:class:`Extent3D` X
:class:`MakeLine` X
:class:`Union` X X X
==================================== ======= ====== ==========
.. rubric:: Footnotes

View File

@ -1090,87 +1090,72 @@ Spatial Aggregates
Aggregate Methods
-----------------
.. deprecated:: 1.8
Aggregate methods are now deprecated. Prefer using their function-based
equivalents.
``collect``
~~~~~~~~~~~
.. method:: GeoQuerySet.collect(**kwargs)
*Availability*: PostGIS, Spatialite (>=3.0)
.. deprecated:: 1.8
Returns a ``GEOMETRYCOLLECTION`` or a ``MULTI`` geometry object from the geometry
column. This is analogous to a simplified version of the :meth:`GeoQuerySet.unionagg` method,
except it can be several orders of magnitude faster than performing a union because
it simply rolls up geometries into a collection or multi object, not caring about
dissolving boundaries.
Use the :class:`Collect` aggregate instead.
Shortcut for ``aggregate(Collect(<field>))``.
``extent``
~~~~~~~~~~
.. method:: GeoQuerySet.extent(**kwargs)
*Availability*: PostGIS, Oracle, Spatialite (>=3.0)
.. deprecated:: 1.8
Returns the extent of the ``GeoQuerySet`` as a four-tuple, comprising the
lower left coordinate and the upper right coordinate.
Use the :class:`Extent` aggregate instead.
Example::
>>> qs = City.objects.filter(name__in=('Houston', 'Dallas'))
>>> print(qs.extent())
(-96.8016128540039, 29.7633724212646, -95.3631439208984, 32.782058715820)
Shortcut for ``aggregate(Extent(<field>))``.
``extent3d``
~~~~~~~~~~~~
.. method:: GeoQuerySet.extent3d(**kwargs)
*Availability*: PostGIS
.. deprecated:: 1.8
Returns the 3D extent of the ``GeoQuerySet`` as a six-tuple, comprising
the lower left coordinate and upper right coordinate.
Use the :class:`Extent` aggregate instead.
Example::
>>> qs = City.objects.filter(name__in=('Houston', 'Dallas'))
>>> print(qs.extent3d())
(-96.8016128540039, 29.7633724212646, 0, -95.3631439208984, 32.782058715820, 0)
Shortcut for ``aggregate(Extent3D(<field>))``.
``make_line``
~~~~~~~~~~~~~
.. method:: GeoQuerySet.make_line(**kwargs)
*Availability*: PostGIS
.. deprecated:: 1.8
Returns a ``LineString`` constructed from the point field geometries in the
``GeoQuerySet``. Currently, ordering the queryset has no effect.
Use the :class:`MakeLine` aggregate instead.
Example::
>>> print(City.objects.filter(name__in=('Houston', 'Dallas')).make_line())
LINESTRING (-95.3631510000000020 29.7633739999999989, -96.8016109999999941 32.7820570000000018)
Shortcut for ``aggregate(MakeLine(<field>))``.
``unionagg``
~~~~~~~~~~~~
.. method:: GeoQuerySet.unionagg(**kwargs)
*Availability*: PostGIS, Oracle, SpatiaLite
.. deprecated:: 1.8
This method returns a :class:`~django.contrib.gis.geos.GEOSGeometry` object
comprising the union of every geometry in the queryset. Please note that
use of ``unionagg`` is processor intensive and may take a significant amount
of time on large querysets.
Use the :class:`Union` aggregate instead.
.. note::
Shortcut for ``aggregate(Union(<field>))``.
If the computation time for using this method is too expensive,
consider using :meth:`GeoQuerySet.collect` instead.
Aggregate Functions
-------------------
Example::
>>> u = Zipcode.objects.unionagg() # This may take a long time.
>>> u = Zipcode.objects.filter(poly__within=bbox).unionagg() # A more sensible approach.
Django provides some GIS-specific aggregate functions. For details on how to
use these aggregate functions, see :doc:`the topic guide on aggregation
</topics/db/aggregation>`.
===================== =====================================================
Keyword Argument Description
@ -1183,9 +1168,6 @@ Keyword Argument Description
__ http://docs.oracle.com/html/B14255_01/sdo_intro.htm#sthref150
Aggregate Functions
-------------------
Example::
>>> from django.contrib.gis.db.models import Extent, Union
@ -1196,35 +1178,84 @@ Example::
.. class:: Collect(geo_field)
Returns the same as the :meth:`GeoQuerySet.collect` aggregate method.
*Availability*: PostGIS, Spatialite (≥3.0)
Returns a ``GEOMETRYCOLLECTION`` or a ``MULTI`` geometry object from the geometry
column. This is analogous to a simplified version of the :class:`Union`
aggregate, except it can be several orders of magnitude faster than performing
a union because it simply rolls up geometries into a collection or multi object,
not caring about dissolving boundaries.
``Extent``
~~~~~~~~~~
.. class:: Extent(geo_field)
*Availability*: PostGIS, Oracle, Spatialite (≥3.0)
Returns the same as the :meth:`GeoQuerySet.extent` aggregate method.
Returns the extent of all ``geo_field`` in the ``QuerySet`` as a four-tuple,
comprising the lower left coordinate and the upper right coordinate.
Example::
>>> qs = City.objects.filter(name__in=('Houston', 'Dallas')).aggregate(Extent('poly'))
>>> print(qs[poly__extent])
(-96.8016128540039, 29.7633724212646, -95.3631439208984, 32.782058715820)
``Extent3D``
~~~~~~~~~~~~
.. class:: Extent3D(geo_field)
Returns the same as the :meth:`GeoQuerySet.extent3d` aggregate method.
*Availability*: PostGIS
Returns the 3D extent of all ``geo_field`` in the ``QuerySet`` as a six-tuple,
comprising the lower left coordinate and upper right coordinate (each with x, y,
and z coordinates).
Example::
>>> qs = City.objects.filter(name__in=('Houston', 'Dallas')).aggregate(Extent3D('poly'))
>>> print(qs[poly__extent3d])
(-96.8016128540039, 29.7633724212646, 0, -95.3631439208984, 32.782058715820, 0)
``MakeLine``
~~~~~~~~~~~~
.. class:: MakeLine(geo_field)
Returns the same as the :meth:`GeoQuerySet.make_line` aggregate method.
*Availability*: PostGIS
Returns a ``LineString`` constructed from the point field geometries in the
``QuerySet``. Currently, ordering the queryset has no effect.
Example::
>>> print(City.objects.filter(name__in=('Houston', 'Dallas')
... ).aggregate(MakeLine('poly'))[poly__makeline]
LINESTRING (-95.3631510000000020 29.7633739999999989, -96.8016109999999941 32.7820570000000018)
``Union``
~~~~~~~~~
.. class:: Union(geo_field)
Returns the same as the :meth:`GeoQuerySet.union` aggregate method.
*Availability*: PostGIS, Oracle, SpatiaLite
This method returns a :class:`~django.contrib.gis.geos.GEOSGeometry` object
comprising the union of every geometry in the queryset. Please note that use of
``Union`` is processor intensive and may take a significant amount of time on
large querysets.
.. note::
If the computation time for using this method is too expensive, consider
using :class:`Collect` instead.
Example::
>>> u = Zipcode.objects.aggregate(Union(poly)) # This may take a long time.
>>> u = Zipcode.objects.filter(poly__within=bbox).aggregate(Union(poly)) # A more sensible approach.
.. rubric:: Footnotes
.. [#fnde9im] *See* `OpenGIS Simple Feature Specification For SQL <http://www.opengis.org/docs/99-049.pdf>`_, at Ch. 2.1.13.2, p. 2-13 (The Dimensionally Extended Nine-Intersection Model).

View File

@ -1580,6 +1580,14 @@ The legacy ``%(<foo>)s`` syntax in :attr:`ModelFormMixin.success_url
<django.views.generic.edit.ModelFormMixin.success_url>` is deprecated and
will be removed in Django 2.0.
``GeoQuerySet`` aggregate methods
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``collect()``, ``extent()``, ``extent3d()``, ``makeline()``, and ``union()``
aggregate methods are deprecated and should be replaced by their function-based
aggregate equivalents (``Collect``, ``Extent``, ``Extent3D``, ``MakeLine``, and
``Union``).
.. removed-features-1.8:
Features removed in 1.8