| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| from shapely.geometry import Polygon |
| from tests import csv_point_input_location, union_polygon_input_location |
| from tests.test_base import TestBase |
| |
| |
| class TestConstructors(TestBase): |
| |
| def test_st_extent(self): |
| df = self.spark.sql(""" |
| SELECT ST_GeomFromWKT(wkt) AS geom |
| FROM VALUES ('POINT (1 2)'), ('POINT (4 5)'), ('LINESTRING (-3 0, 0 0)') AS t(wkt) |
| """) |
| df.createOrReplaceTempView("extent_input") |
| bbox = self.spark.sql("SELECT ST_Extent(geom) FROM extent_input").first()[0] |
| assert bbox.xmin == -3.0 |
| assert bbox.ymin == 0.0 |
| assert bbox.xmax == 4.0 |
| assert bbox.ymax == 5.0 |
| |
| def test_st_extent_returns_null_for_empty_input(self): |
| df = self.spark.sql(""" |
| SELECT ST_GeomFromWKT(wkt) AS geom |
| FROM VALUES (CAST(NULL AS STRING)), ('POINT EMPTY') AS t(wkt) |
| """) |
| df.createOrReplaceTempView("empty_extent_input") |
| result = self.spark.sql( |
| "SELECT ST_Extent(geom) FROM empty_extent_input" |
| ).first()[0] |
| assert result is None |
| |
| def test_st_envelope_aggr(self): |
| point_csv_df = ( |
| self.spark.read.format("csv") |
| .option("delimiter", ",") |
| .option("header", "false") |
| .load(csv_point_input_location) |
| ) |
| |
| point_csv_df.createOrReplaceTempView("pointtable") |
| point_df = self.spark.sql( |
| "select ST_Point(cast(pointtable._c0 as Decimal(24,20)), cast(pointtable._c1 as Decimal(24,20))) as arealandmark from pointtable" |
| ) |
| point_df.createOrReplaceTempView("pointdf") |
| boundary = self.spark.sql( |
| "select ST_Envelope_Aggr(pointdf.arealandmark) from pointdf" |
| ) |
| |
| coordinates = [ |
| (1.1, 101.1), |
| (1.1, 1100.1), |
| (1000.1, 1100.1), |
| (1000.1, 101.1), |
| (1.1, 101.1), |
| ] |
| |
| polygon = Polygon(coordinates) |
| |
| assert boundary.take(1)[0][0] == polygon |
| |
| def test_st_union_aggr(self): |
| polygon_csv_df = ( |
| self.spark.read.format("csv") |
| .option("delimiter", ",") |
| .option("header", "false") |
| .load(union_polygon_input_location) |
| ) |
| |
| polygon_csv_df.createOrReplaceTempView("polygontable") |
| polygon_csv_df.show() |
| polygon_df = self.spark.sql( |
| "select ST_PolygonFromEnvelope(cast(polygontable._c0 as Decimal(24,20)),cast(polygontable._c1 as Decimal(24,20)), cast(polygontable._c2 as Decimal(24,20)), cast(polygontable._c3 as Decimal(24,20))) as polygonshape from polygontable" |
| ) |
| polygon_df.createOrReplaceTempView("polygondf") |
| polygon_df.show() |
| union = self.spark.sql( |
| "select ST_Union_Aggr(polygondf.polygonshape) from polygondf" |
| ) |
| |
| assert union.take(1)[0][0].area == 10100 |
| |
| def test_st_collect_aggr_points(self): |
| self.spark.sql(""" |
| SELECT explode(array( |
| ST_GeomFromWKT('POINT(1 2)'), |
| ST_GeomFromWKT('POINT(3 4)'), |
| ST_GeomFromWKT('POINT(5 6)') |
| )) AS geom |
| """).createOrReplaceTempView("points_table") |
| |
| result = self.spark.sql("SELECT ST_Collect_Agg(geom) FROM points_table").take( |
| 1 |
| )[0][0] |
| |
| assert result.geom_type == "MultiPoint" |
| assert len(result.geoms) == 3 |
| |
| def test_st_collect_aggr_polygons(self): |
| self.spark.sql(""" |
| SELECT explode(array( |
| ST_GeomFromWKT('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))'), |
| ST_GeomFromWKT('POLYGON((2 2, 3 2, 3 3, 2 3, 2 2))') |
| )) AS geom |
| """).createOrReplaceTempView("polygons_table") |
| |
| result = self.spark.sql("SELECT ST_Collect_Agg(geom) FROM polygons_table").take( |
| 1 |
| )[0][0] |
| |
| assert result.geom_type == "MultiPolygon" |
| assert len(result.geoms) == 2 |
| assert result.area == 2.0 |
| |
| def test_st_collect_aggr_mixed_types(self): |
| self.spark.sql(""" |
| SELECT explode(array( |
| ST_GeomFromWKT('POINT(1 2)'), |
| ST_GeomFromWKT('LINESTRING(0 0, 1 1)'), |
| ST_GeomFromWKT('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))') |
| )) AS geom |
| """).createOrReplaceTempView("mixed_geom_table") |
| |
| result = self.spark.sql( |
| "SELECT ST_Collect_Agg(geom) FROM mixed_geom_table" |
| ).take(1)[0][0] |
| |
| assert result.geom_type == "GeometryCollection" |
| assert len(result.geoms) == 3 |
| |
| def test_st_collect_aggr_preserves_duplicates(self): |
| # Test that ST_Collect_Agg keeps duplicate geometries (unlike ST_Union_Aggr) |
| self.spark.sql(""" |
| SELECT explode(array( |
| ST_GeomFromWKT('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))'), |
| ST_GeomFromWKT('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))') |
| )) AS geom |
| """).createOrReplaceTempView("duplicate_polygons_table") |
| |
| result = self.spark.sql( |
| "SELECT ST_Collect_Agg(geom) FROM duplicate_polygons_table" |
| ).take(1)[0][0] |
| |
| # ST_Collect_Agg should preserve both polygons |
| assert len(result.geoms) == 2 |
| # Area should be 2 because it doesn't merge overlapping areas |
| assert result.area == 2.0 |
| |
| # Test aliases for *_Aggr functions with *_Agg suffix |
| def test_st_envelope_agg_alias(self): |
| self.spark.sql(""" |
| SELECT explode(array( |
| ST_GeomFromWKT('POINT(1.1 101.1)'), |
| ST_GeomFromWKT('POINT(1.1 1100.1)'), |
| ST_GeomFromWKT('POINT(1000.1 1100.1)'), |
| ST_GeomFromWKT('POINT(1000.1 101.1)') |
| )) AS arealandmark |
| """).createOrReplaceTempView("pointdf_alias") |
| |
| boundary = self.spark.sql( |
| "SELECT ST_Envelope_Agg(pointdf_alias.arealandmark) FROM pointdf_alias" |
| ) |
| |
| coordinates = [ |
| (1.1, 101.1), |
| (1.1, 1100.1), |
| (1000.1, 1100.1), |
| (1000.1, 101.1), |
| (1.1, 101.1), |
| ] |
| |
| polygon = Polygon(coordinates) |
| assert boundary.take(1)[0][0].equals(polygon) |
| |
| def test_st_intersection_agg_alias(self): |
| self.spark.sql(""" |
| SELECT explode(array( |
| ST_GeomFromWKT('POLYGON((0 0, 4 0, 4 4, 0 4, 0 0))'), |
| ST_GeomFromWKT('POLYGON((2 2, 6 2, 6 6, 2 6, 2 2))') |
| )) AS countyshape |
| """).createOrReplaceTempView("polygondf_alias") |
| |
| intersection = self.spark.sql( |
| "SELECT ST_Intersection_Agg(polygondf_alias.countyshape) FROM polygondf_alias" |
| ) |
| |
| result = intersection.take(1)[0][0] |
| # The intersection of the two polygons should be a square from (2,2) to (4,4) with area 4 |
| assert result.area == 4.0 |
| |
| def test_st_union_agg_alias(self): |
| self.spark.sql(""" |
| SELECT explode(array( |
| ST_GeomFromWKT('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))'), |
| ST_GeomFromWKT('POLYGON((1 1, 3 1, 3 3, 1 3, 1 1))') |
| )) AS countyshape |
| """).createOrReplaceTempView("polygondf_union_alias") |
| |
| union = self.spark.sql( |
| "SELECT ST_Union_Agg(polygondf_union_alias.countyshape) FROM polygondf_union_alias" |
| ) |
| |
| result = union.take(1)[0][0] |
| # Two overlapping 2x2 squares with 1x1 overlap: area = 4 + 4 - 1 = 7 |
| assert result.area == 7.0 |