diff --git a/data/geospatial/README.md b/data/geospatial/README.md new file mode 100644 index 0000000..5798fc4 --- /dev/null +++ b/data/geospatial/README.md @@ -0,0 +1,67 @@ + + +# Geospatial Test Files + + +These test files cover the main and corner case functionality of the +[Parquet Geospatial Types](https://github.com/apache/parquet-format/blob/master/Geospatial.md) +GEOMETRY and GEOGRAPHY. + +- `geospatial.parquet`: Contains row groups with specific combinations of + geometry types to test statistics generation and geometry type coverage. + The file contains columns `group` (string identifier of the group name), + `wkt` (the human-readable well-known text representation of the geometry) + and `geometry` (a Parquet GEOMETRY column). A human-readable version of + the file is available in `geospatial.yaml`. + +- `geospatial-with-nan.parquet`: Contains a single row group with a GEOMETRY + column whose contents contains two valid geometries and one invalid LINESTRING + whose coordinates contain a `NaN` value in all dimensions. Such a geometry is + not valid and the behaviour of it is not defined; however, implementations should + not generate statistics that would prevent the other (valid) geometries in the + column chunk from appearing in the case of predicate pushdown. Notably, + implementations should *not* generate statistics that contain `NaN` for this case. + + Note that POINT EMPTY is represented by convention in well-known binary as + a POINT whose coordinates are all `NaN`, which should be treated as a valid + (but empty) geometry. + +- `crs-default.parquet`: Contains a GEOMETRY column with the crs + omitted. This should be interpreted as OGC:CRS84 (i.e., longitude/latitude). + +- `crs-geography.parquet`: Contains a GEOGRAPHY column with the crs + omitted. This should be interpreted as OGC:CRS84 (i.e., longitude/latitude). + +- `crs-projjson.parquet`: Contains a GEOMETRY column with the crs parameter + set to `projjson:projjson_epsg_5070` and a metadata field with the key + `projjson_epsg_5070` and a value consisting of the appropriate PROJJSON + value for EPSG:5070. + +- `crs-srid.parquet`: Contains a GEOMETRY column with the crs parameter set + to `srid:5070`. The Parquet format does not mention the EPSG database in + any way, but otherwise out-of-context SRID values are commonly interpreted + as the corresponding EPSG:xxxx value. Producers of SRIDs may wish to + avoid valid EPSG:xxxx values where this is not the intended usage to minimize + the chances they will be misinterpreted by consumers who make this assumption. + +- `crs-arbitrary-value.parquet`: Contains a GEOMETRY column with the crs + parameter set to an arbitrary string value. The Parquet format does not + restrict the value of the crs parameter and implementations may choose to + attempt interpreting the value or error. diff --git a/data/geospatial/crs-arbitrary-value.parquet b/data/geospatial/crs-arbitrary-value.parquet new file mode 100644 index 0000000..622049c Binary files /dev/null and b/data/geospatial/crs-arbitrary-value.parquet differ diff --git a/data/geospatial/crs-default.parquet b/data/geospatial/crs-default.parquet new file mode 100644 index 0000000..280dc2c Binary files /dev/null and b/data/geospatial/crs-default.parquet differ diff --git a/data/geospatial/crs-gen.py b/data/geospatial/crs-gen.py new file mode 100644 index 0000000..48f849e --- /dev/null +++ b/data/geospatial/crs-gen.py @@ -0,0 +1,163 @@ +# 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. + +import json + +from pathlib import Path + +import pyarrow as pa +from pyarrow import parquet +import pyproj +import shapely + +HERE = Path(__file__).parent + + +# Using Wyoming because it is the easiest state to inline into a Python file +WYOMING_LOWRES = ( + "POLYGON ((-111.0 45.0, -111.0 41.0, -104.0 41.0, -104.0 45.0, -111.0 45.0))" +) + +# We densify the edges such that there is a point every 0.1 degrees to minimize +# the effect of the edge algorithm and coordinate transformation. +WYOMING_HIRES = shapely.from_wkt(WYOMING_LOWRES).segmentize(0.1).wkt + + +class WkbType(pa.ExtensionType): + """Minimal geoarrow.wkb implementation""" + + def __init__(self, crs=None, edges=None, *, storage_type=pa.binary(), **kwargs): + self.crs = crs + self.edges = edges + super().__init__(storage_type, "geoarrow.wkb") + + def __arrow_ext_serialize__(self): + obj = {"crs": self.crs, "edges": self.edges} + return json.dumps({k: v for k, v in obj.items() if v}).encode() + + @classmethod + def __arrow_ext_deserialize__(cls, storage_type, serialized): + obj: dict = json.loads(serialized) + return WkbType(**obj, storage_type=storage_type) + + +pa.register_extension_type(WkbType()) + + +def write_crs(type, geometry, name, col_name="geometry", metadata=None): + schema = pa.schema({"wkt": pa.utf8(), col_name: type}) + + with parquet.ParquetWriter( + HERE / name, + schema, + # Not sure if there's a way to write metadata without + # storing the Arrow schema + store_schema=metadata is not None, + compression="none", + ) as writer: + batch = pa.record_batch( + { + "wkt": [geometry.wkt], + col_name: type.wrap_array(pa.array([geometry.wkb])), + } + ) + writer.write_batch(batch) + + if metadata is not None: + writer.add_key_value_metadata(metadata) + + +def write_crs_files(): + # Create the Shapely geometry + geometry = shapely.from_wkt(WYOMING_HIRES) + + # A general purpose coordinate system for the United States + crs_not_lonlat = pyproj.CRS("EPSG:5070") + transformer = pyproj.Transformer.from_crs( + "OGC:CRS84", crs_not_lonlat, always_xy=True + ) + geometry_not_lonlat = shapely.transform( + geometry, transformer.transform, interleaved=False + ) + + # Write with the default CRS (i.e., lon/lat) + write_crs(WkbType(), geometry, "crs-default.parquet") + + # Write a Geography column with the default CRS + write_crs( + WkbType(edges="spherical"), + geometry, + "crs-geography.parquet", + col_name="geography", + ) + + # Write a file with the projjson format in the specification + # and the appropriate metadata key + write_crs( + WkbType(crs="projjson:projjson_epsg_5070"), + geometry_not_lonlat, + "crs-projjson.parquet", + metadata={"projjson_epsg_5070": crs_not_lonlat.to_json()}, + ) + + # Write a file with the srid format in the specification + write_crs(WkbType(crs="srid:5070"), geometry_not_lonlat, "crs-srid.parquet") + + # Write a file with an arbitrary value (theoretically allowed by the format + # and consumers may choose to error or attempt to interpret the value) + write_crs( + WkbType(crs=crs_not_lonlat.to_json_dict()), + geometry_not_lonlat, + "crs-arbitrary-value.parquet", + ) + + +def check_crs_schema(name, expected_col_type): + file = parquet.ParquetFile(HERE / name) + + col = file.schema.column(1) + col_dict = json.loads(col.logical_type.to_json()) + col_type = col_dict["Type"] + if col_type != expected_col_type: + raise ValueError( + f"Expected '{expected_col_type}' logical type but got '{col_type}'" + ) + + +def check_crs_crs(name, expected_crs): + expected_crs = pyproj.CRS(expected_crs) + + file = parquet.ParquetFile(HERE / name, arrow_extensions_enabled=True) + ext_type = file.schema_arrow.field(1).type + actual_crs = pyproj.CRS(ext_type.crs) + if actual_crs != expected_crs: + raise ValueError(f"Expected '{expected_crs}' crs but got '{actual_crs}'") + + +def check_crs(name, expected_col_type, expected_crs): + check_crs_schema(name, expected_col_type) + check_crs_crs(name, expected_crs) + + +if __name__ == "__main__": + write_crs_files() + + check_crs("crs-default.parquet", "Geometry", "OGC:CRS84") + check_crs("crs-geography.parquet", "Geography", "OGC:CRS84") + check_crs("crs-projjson.parquet", "Geometry", "EPSG:5070") + check_crs("crs-srid.parquet", "Geometry", "EPSG:5070") + check_crs("crs-arbitrary-value.parquet", "Geometry", "EPSG:5070") diff --git a/data/geospatial/crs-geography.parquet b/data/geospatial/crs-geography.parquet new file mode 100644 index 0000000..286e2ec Binary files /dev/null and b/data/geospatial/crs-geography.parquet differ diff --git a/data/geospatial/crs-projjson.parquet b/data/geospatial/crs-projjson.parquet new file mode 100644 index 0000000..8d694bc Binary files /dev/null and b/data/geospatial/crs-projjson.parquet differ diff --git a/data/geospatial/crs-srid.parquet b/data/geospatial/crs-srid.parquet new file mode 100644 index 0000000..0b97a53 Binary files /dev/null and b/data/geospatial/crs-srid.parquet differ diff --git a/data/geospatial/geospatial-gen.py b/data/geospatial/geospatial-gen.py new file mode 100644 index 0000000..3d3ee5a --- /dev/null +++ b/data/geospatial/geospatial-gen.py @@ -0,0 +1,213 @@ +# 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. + +import json +from pathlib import Path +import re + +import geoarrow.pyarrow as ga +import numpy as np +import pyarrow as pa +from pyarrow import parquet +import shapely +import shapely.testing +import yaml + +# M value support was added in Shapely 2.1.0 +if tuple(shapely.__version__.split(".")) < ("2", "1", "0"): + raise ImportError("shapely >= 2.1.0 is required") + +HERE = Path(__file__).parent + +# Shapely doesn't propagate the geometry type for MULTI* Z/M/ZM, so +# we have to write our own geometry type detector to check statistics output +GEOMETRY_TYPE_CODE = { + "POINT": 1, + "LINESTRING": 2, + "POLYGON": 3, + "MULTIPOINT": 4, + "MULTILINESTRING": 5, + "MULTIPOLYGON": 6, + "GEOMETRYCOLLECTION": 7, +} + +DIMENSIONS_CODE = {None: 0, "Z": 1000, "M": 2000, "ZM": 3000} + + +def geometry_type_code(wkt): + if wkt is None: + return None + + geometry_type, _, dimensions = re.match(r"([A-Z]+)( ([ZM]+)?)?", wkt).groups() + return GEOMETRY_TYPE_CODE[geometry_type] + DIMENSIONS_CODE[dimensions] + + +def write_geospatial(): + with open(HERE / "geospatial.yaml") as f: + examples = yaml.safe_load(f) + + schema = pa.schema({"group": pa.utf8(), "wkt": pa.utf8(), "geometry": ga.wkb()}) + + with parquet.ParquetWriter( + HERE / "geospatial.parquet", + schema, + store_schema=False, + compression="none", + ) as writer: + for group_name, geometries_wkt in examples.items(): + # Unfortunately we can't use Shapely to generate the test WKB + # because of https://github.com/libgeos/geos/issues/888, so we use + # geoarrow.pyarrow.as_wkb() instead. + # geometries = shapely.from_wkt(geometries_wkt) + # wkbs = shapely.to_wkb(geometries, flavor="iso") + # wkb_array = ga.wkb().wrap_array(pa.array(wkbs, pa.binary())) + wkt_array = pa.array(geometries_wkt, pa.utf8()) + + batch = pa.record_batch( + { + "group": [group_name] * len(geometries_wkt), + "wkt": wkt_array, + "geometry": ga.as_wkb(wkt_array), + } + ) + + writer.write_batch(batch) + + +def write_geospatial_with_nan(): + geometries_wkt = [ + "POINT ZM (10 20 30 40)", + "POINT ZM (50 60 70 80)", + "LINESTRING ZM (90 100 110 120, nan nan nan nan, 130 140 150 160)", + ] + + schema = pa.schema({"group": pa.utf8(), "wkt": pa.utf8(), "geometry": ga.wkb()}) + + with parquet.ParquetWriter( + HERE / "geospatial-with-nan.parquet", + schema, + store_schema=False, + compression="none", + ) as writer: + geometries = shapely.from_wkt(geometries_wkt) + wkbs = shapely.to_wkb(geometries, flavor="iso") + wkb_array = ga.wkb().wrap_array(pa.array(wkbs, pa.binary())) + + batch = pa.record_batch( + { + "group": ["with-nan"] * len(geometries_wkt), + "wkt": geometries_wkt, + "geometry": wkb_array, + } + ) + + writer.write_batch(batch) + + +def check_geospatial_schema(name): + file = parquet.ParquetFile(HERE / name) + + col = file.schema.column(2) + col_dict = json.loads(col.logical_type.to_json()) + col_type = col_dict["Type"] + if col_type != "Geometry": + raise ValueError(f"Expected 'Geometry' logical type but got '{col_type}'") + + +def check_geospatial_values(name): + tab = parquet.read_table(HERE / name, arrow_extensions_enabled=False) + geometries_from_wkt = shapely.from_wkt(tab["wkt"]) + geometries_from_wkb = shapely.from_wkb(tab["geometry"]) + shapely.testing.assert_geometries_equal(geometries_from_wkt, geometries_from_wkb) + + +def iso_type_code(shapely_type_id, has_z, has_m): + # item was null + if shapely_type_id < 0: + return None + + # GEOS type ids are not quite WKB type ids + iso_type_id = shapely_type_id if shapely_type_id >= 3 else shapely_type_id + 1 + iso_dimensions = ( + 3000 if has_z and has_m else 2000 if has_m else 1000 if has_z else 0 + ) + return int(iso_dimensions + iso_type_id) + + +def calc_stats_shapely(wkts): + geometries = shapely.from_wkt(wkts) + + # Calculate the list of iso type codes + any_not_null = any(geom is not None for geom in geometries) + type_codes = set(geometry_type_code(wkt) for wkt in wkts) + + # Calculate min/max ignoring nan values + coords = shapely.get_coordinates(geometries, include_z=True, include_m=True) + coord_mins = [ + None if np.isposinf(x) else float(x) + for x in np.nanmin(coords, 0, initial=np.inf) + ] + coord_maxes = [ + None if np.isneginf(x) else float(x) + for x in np.nanmax(coords, 0, initial=-np.inf) + ] + + # Assemble stats in the same format as returned by geo_statistics.to_dict() + stats = {} + stats["geospatial_types"] = list( + sorted(code for code in type_codes if code is not None) + ) if any_not_null else None + stats["xmin"], stats["ymin"], stats["zmin"], stats["mmin"] = coord_mins + stats["xmax"], stats["ymax"], stats["zmax"], stats["mmax"] = coord_maxes + + return stats + + +def check_batch_statistics(stats, wkts, group): + if stats is None: + raise ValueError(f"geo_statistics is missing for group {group}") + + shapely_stats = calc_stats_shapely(wkts) + file_stats = stats.to_dict() + if file_stats != shapely_stats: + raise ValueError( + f"stats mismatch calculated:\n{shapely_stats}\nvs file\n{file_stats}" + ) + + +def check_geospatial_statistics(name): + file = parquet.ParquetFile(HERE / name) + for i in range(file.num_row_groups): + batch = file.read_row_group(i) + column_metadata = file.metadata.row_group(i).column(2) + group = batch["group"][0].as_py() + + check_batch_statistics( + column_metadata.geo_statistics, batch["wkt"].to_pylist(), group + ) + + +if __name__ == "__main__": + write_geospatial() + write_geospatial_with_nan() + + check_geospatial_schema("geospatial.parquet") + check_geospatial_values("geospatial.parquet") + check_geospatial_statistics("geospatial.parquet") + + check_geospatial_schema("geospatial-with-nan.parquet") + check_geospatial_statistics("geospatial-with-nan.parquet") diff --git a/data/geospatial/geospatial-with-nan.parquet b/data/geospatial/geospatial-with-nan.parquet new file mode 100644 index 0000000..995139c Binary files /dev/null and b/data/geospatial/geospatial-with-nan.parquet differ diff --git a/data/geospatial/geospatial.parquet b/data/geospatial/geospatial.parquet new file mode 100644 index 0000000..eb5ed5c Binary files /dev/null and b/data/geospatial/geospatial.parquet differ diff --git a/data/geospatial/geospatial.yaml b/data/geospatial/geospatial.yaml new file mode 100644 index 0000000..0ac5c96 --- /dev/null +++ b/data/geospatial/geospatial.yaml @@ -0,0 +1,332 @@ +# 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. + +# These are the values used to generate the content of the geospatial.parquet +# file, with each item here comprising a row group in the output. (See +# README.md for further description of geospatial.parquet). Note that +# Z values are always calculated as X + Y and M values are always calculated +# as Z * Y for the purposes of this example data. + +# Contains one non-empty geometry of every geometry type/dimensions combination +all: +- POINT (30 10) +- LINESTRING (30 10, 10 30, 40 40) +- POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10)) +- MULTIPOINT ((30 10)) +- MULTILINESTRING ((30 10, 10 30, 40 40)) +- MULTIPOLYGON (((30 10, 40 40, 20 40, 10 20, 30 10))) +- GEOMETRYCOLLECTION (POINT (30 10), LINESTRING (30 10, 10 30, 40 40), POLYGON ((30 + 10, 40 40, 20 40, 10 20, 30 10)), MULTIPOINT ((30 10)), MULTILINESTRING ((30 10, + 10 30, 40 40)), MULTIPOLYGON (((30 10, 40 40, 20 40, 10 20, 30 10)))) +- POINT Z (30 10 40) +- LINESTRING Z (30 10 40, 10 30 40, 40 40 80) +- POLYGON Z ((30 10 40, 40 40 80, 20 40 60, 10 20 30, 30 10 40)) +- MULTIPOINT Z ((30 10 40)) +- MULTILINESTRING Z ((30 10 40, 10 30 40, 40 40 80)) +- MULTIPOLYGON Z (((30 10 40, 40 40 80, 20 40 60, 10 20 30, 30 10 40))) +- GEOMETRYCOLLECTION Z (POINT Z (30 10 40), LINESTRING Z (30 10 40, 10 30 40, 40 40 + 80), POLYGON Z ((30 10 40, 40 40 80, 20 40 60, 10 20 30, 30 10 40)), MULTIPOINT + Z ((30 10 40)), MULTILINESTRING Z ((30 10 40, 10 30 40, 40 40 80)), MULTIPOLYGON + Z (((30 10 40, 40 40 80, 20 40 60, 10 20 30, 30 10 40)))) +- POINT M (30 10 300) +- LINESTRING M (30 10 300, 10 30 300, 40 40 1600) +- POLYGON M ((30 10 300, 40 40 1600, 20 40 800, 10 20 200, 30 10 300)) +- MULTIPOINT M ((30 10 300)) +- MULTILINESTRING M ((30 10 300, 10 30 300, 40 40 1600)) +- MULTIPOLYGON M (((30 10 300, 40 40 1600, 20 40 800, 10 20 200, 30 10 300))) +- GEOMETRYCOLLECTION M (POINT M (30 10 300), LINESTRING M (30 10 300, 10 30 300, 40 + 40 1600), POLYGON M ((30 10 300, 40 40 1600, 20 40 800, 10 20 200, 30 10 300)), + MULTIPOINT M ((30 10 300)), MULTILINESTRING M ((30 10 300, 10 30 300, 40 40 1600)), + MULTIPOLYGON M (((30 10 300, 40 40 1600, 20 40 800, 10 20 200, 30 10 300)))) +- POINT ZM (30 10 40 300) +- LINESTRING ZM (30 10 40 300, 10 30 40 300, 40 40 80 1600) +- POLYGON ZM ((30 10 40 300, 40 40 80 1600, 20 40 60 800, 10 20 30 200, 30 10 40 300)) +- MULTIPOINT ZM ((30 10 40 300)) +- MULTILINESTRING ZM ((30 10 40 300, 10 30 40 300, 40 40 80 1600)) +- MULTIPOLYGON ZM (((30 10 40 300, 40 40 80 1600, 20 40 60 800, 10 20 30 200, 30 10 + 40 300))) +- GEOMETRYCOLLECTION ZM (POINT ZM (30 10 40 300), LINESTRING ZM (30 10 40 300, 10 + 30 40 300, 40 40 80 1600), POLYGON ZM ((30 10 40 300, 40 40 80 1600, 20 40 60 800, + 10 20 30 200, 30 10 40 300)), MULTIPOINT ZM ((30 10 40 300)), MULTILINESTRING ZM + ((30 10 40 300, 10 30 40 300, 40 40 80 1600)), MULTIPOLYGON ZM (((30 10 40 300, + 40 40 80 1600, 20 40 60 800, 10 20 30 200, 30 10 40 300)))) + +# Contains one empty geometry of every geometry type/dimensions combination +empty-geometries: +- POINT EMPTY +- LINESTRING EMPTY +- POLYGON EMPTY +- MULTIPOINT EMPTY +- MULTILINESTRING EMPTY +- MULTIPOLYGON EMPTY +- GEOMETRYCOLLECTION EMPTY +- POINT Z EMPTY +- LINESTRING Z EMPTY +- POLYGON Z EMPTY +- MULTIPOINT Z EMPTY +- MULTILINESTRING Z EMPTY +- MULTIPOLYGON Z EMPTY +- GEOMETRYCOLLECTION Z EMPTY +- POINT M EMPTY +- LINESTRING M EMPTY +- POLYGON M EMPTY +- MULTIPOINT M EMPTY +- MULTILINESTRING M EMPTY +- MULTIPOLYGON M EMPTY +- GEOMETRYCOLLECTION M EMPTY +- POINT ZM EMPTY +- LINESTRING ZM EMPTY +- POLYGON ZM EMPTY +- MULTIPOINT ZM EMPTY +- MULTILINESTRING ZM EMPTY +- MULTIPOLYGON ZM EMPTY +- GEOMETRYCOLLECTION ZM EMPTY + +# Contains only null values +null-geometries: +- null +- null +- null +- null + +# Individual row groups for each geometry type/dimensions combination. +# Each contains at least two non-empty items, a null, and an EMPTY. +point: +- POINT (30 10) +- POINT (40 20) +- null +- POINT EMPTY + +linestring: +- LINESTRING (30 10, 10 30, 40 40) +- LINESTRING (40 20, 20 40, 50 50) +- null +- LINESTRING EMPTY + +polygon: +- POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10)) +- POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30)) +- null +- POLYGON EMPTY + +multipoint: +- MULTIPOINT ((30 10)) +- MULTIPOINT ((10 40), (40 30), (20 20), (30 10)) +- null +- MULTIPOINT EMPTY + +multilinestring: +- MULTILINESTRING ((30 10, 10 30, 40 40)) +- MULTILINESTRING ((10 10, 20 20, 10 40), (40 40, 30 30, 40 20, 30 10)) +- null +- MULTILINESTRING EMPTY + +multipolygon: +- MULTIPOLYGON (((30 10, 40 40, 20 40, 10 20, 30 10))) +- MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)), ((15 5, 40 10, 10 20, 5 10, 15 5))) +- MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)), ((20 35, 10 30, 10 10, 30 5, 45 20, + 20 35), (30 20, 20 15, 20 25, 30 20))) +- null +- MULTIPOLYGON EMPTY + +geometrycollection: +- GEOMETRYCOLLECTION (POINT (30 10)) +- GEOMETRYCOLLECTION (LINESTRING (30 10, 10 30, 40 40)) +- GEOMETRYCOLLECTION (POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))) +- GEOMETRYCOLLECTION (MULTIPOINT ((30 10))) +- GEOMETRYCOLLECTION (MULTILINESTRING ((30 10, 10 30, 40 40))) +- GEOMETRYCOLLECTION (MULTIPOLYGON (((30 10, 40 40, 20 40, 10 20, 30 10)))) +- GEOMETRYCOLLECTION (POINT (30 10), LINESTRING (30 10, 10 30, 40 40), POLYGON ((30 + 10, 40 40, 20 40, 10 20, 30 10)), MULTIPOINT ((30 10)), MULTILINESTRING ((30 10, + 10 30, 40 40)), MULTIPOLYGON (((30 10, 40 40, 20 40, 10 20, 30 10)))) +- null +- GEOMETRYCOLLECTION EMPTY + +point-z: +- POINT Z (30 10 40) +- POINT Z (40 20 60) +- null +- POINT Z EMPTY + +linestring-z: +- LINESTRING Z (30 10 40, 10 30 40, 40 40 80) +- LINESTRING Z (40 20 60, 20 40 60, 50 50 100) +- null +- LINESTRING Z EMPTY + +polygon-z: +- POLYGON Z ((30 10 40, 40 40 80, 20 40 60, 10 20 30, 30 10 40)) +- POLYGON Z ((35 10 45, 45 45 90, 15 40 55, 10 20 30, 35 10 45), (20 30 50, 35 35 + 70, 30 20 50, 20 30 50)) +- null +- POLYGON Z EMPTY + +multipoint-z: +- MULTIPOINT Z ((30 10 40)) +- MULTIPOINT Z ((10 40 50), (40 30 70), (20 20 40), (30 10 40)) +- null +- MULTIPOINT Z EMPTY + +multilinestring-z: +- MULTILINESTRING Z ((30 10 40, 10 30 40, 40 40 80)) +- MULTILINESTRING Z ((10 10 20, 20 20 40, 10 40 50), (40 40 80, 30 30 60, 40 20 60, + 30 10 40)) +- null +- MULTILINESTRING Z EMPTY + +multipolygon-z: +- MULTIPOLYGON Z (((30 10 40, 40 40 80, 20 40 60, 10 20 30, 30 10 40))) +- MULTIPOLYGON Z (((30 20 50, 45 40 85, 10 40 50, 30 20 50)), ((15 5 20, 40 10 50, + 10 20 30, 5 10 15, 15 5 20))) +- MULTIPOLYGON Z (((40 40 80, 20 45 65, 45 30 75, 40 40 80)), ((20 35 55, 10 30 40, + 10 10 20, 30 5 35, 45 20 65, 20 35 55), (30 20 50, 20 15 35, 20 25 45, 30 20 50))) +- null +- MULTIPOLYGON Z EMPTY + +geometrycollection-z: +- GEOMETRYCOLLECTION Z (POINT Z (30 10 40)) +- GEOMETRYCOLLECTION Z (LINESTRING Z (30 10 40, 10 30 40, 40 40 80)) +- GEOMETRYCOLLECTION Z (POLYGON Z ((30 10 40, 40 40 80, 20 40 60, 10 20 30, 30 10 + 40))) +- GEOMETRYCOLLECTION Z (MULTIPOINT Z ((30 10 40))) +- GEOMETRYCOLLECTION Z (MULTILINESTRING Z ((30 10 40, 10 30 40, 40 40 80))) +- GEOMETRYCOLLECTION Z (MULTIPOLYGON Z (((30 10 40, 40 40 80, 20 40 60, 10 20 30, + 30 10 40)))) +- GEOMETRYCOLLECTION Z (POINT Z (30 10 40), LINESTRING Z (30 10 40, 10 30 40, 40 40 + 80), POLYGON Z ((30 10 40, 40 40 80, 20 40 60, 10 20 30, 30 10 40)), MULTIPOINT + Z ((30 10 40)), MULTILINESTRING Z ((30 10 40, 10 30 40, 40 40 80)), MULTIPOLYGON + Z (((30 10 40, 40 40 80, 20 40 60, 10 20 30, 30 10 40)))) +- null +- GEOMETRYCOLLECTION Z EMPTY + +point-m: +- POINT M (30 10 300) +- POINT M (40 20 800) +- null +- POINT M EMPTY + +linestring-m: +- LINESTRING M (30 10 300, 10 30 300, 40 40 1600) +- LINESTRING M (40 20 800, 20 40 800, 50 50 2500) +- null +- LINESTRING M EMPTY + +polygon-m: +- POLYGON M ((30 10 300, 40 40 1600, 20 40 800, 10 20 200, 30 10 300)) +- POLYGON M ((35 10 350, 45 45 2025, 15 40 600, 10 20 200, 35 10 350), (20 30 600, + 35 35 1225, 30 20 600, 20 30 600)) +- null +- POLYGON M EMPTY + +multipoint-m: +- MULTIPOINT M ((30 10 300)) +- MULTIPOINT M ((10 40 400), (40 30 1200), (20 20 400), (30 10 300)) +- null +- MULTIPOINT M EMPTY + +multilinestring-m: +- MULTILINESTRING M ((30 10 300, 10 30 300, 40 40 1600)) +- MULTILINESTRING M ((10 10 100, 20 20 400, 10 40 400), (40 40 1600, 30 30 900, 40 + 20 800, 30 10 300)) +- null +- MULTILINESTRING M EMPTY + +multipolygon-m: +- MULTIPOLYGON M (((30 10 300, 40 40 1600, 20 40 800, 10 20 200, 30 10 300))) +- MULTIPOLYGON M (((30 20 600, 45 40 1800, 10 40 400, 30 20 600)), ((15 5 75, 40 10 + 400, 10 20 200, 5 10 50, 15 5 75))) +- MULTIPOLYGON M (((40 40 1600, 20 45 900, 45 30 1350, 40 40 1600)), ((20 35 700, + 10 30 300, 10 10 100, 30 5 150, 45 20 900, 20 35 700), (30 20 600, 20 15 300, 20 + 25 500, 30 20 600))) +- null +- MULTIPOLYGON M EMPTY + +geometrycollection-m: +- GEOMETRYCOLLECTION M (POINT M (30 10 300)) +- GEOMETRYCOLLECTION M (LINESTRING M (30 10 300, 10 30 300, 40 40 1600)) +- GEOMETRYCOLLECTION M (POLYGON M ((30 10 300, 40 40 1600, 20 40 800, 10 20 200, 30 + 10 300))) +- GEOMETRYCOLLECTION M (MULTIPOINT M ((30 10 300))) +- GEOMETRYCOLLECTION M (MULTILINESTRING M ((30 10 300, 10 30 300, 40 40 1600))) +- GEOMETRYCOLLECTION M (MULTIPOLYGON M (((30 10 300, 40 40 1600, 20 40 800, 10 20 + 200, 30 10 300)))) +- GEOMETRYCOLLECTION M (POINT M (30 10 300), LINESTRING M (30 10 300, 10 30 300, 40 + 40 1600), POLYGON M ((30 10 300, 40 40 1600, 20 40 800, 10 20 200, 30 10 300)), + MULTIPOINT M ((30 10 300)), MULTILINESTRING M ((30 10 300, 10 30 300, 40 40 1600)), + MULTIPOLYGON M (((30 10 300, 40 40 1600, 20 40 800, 10 20 200, 30 10 300)))) +- null +- GEOMETRYCOLLECTION M EMPTY + +point-zm: +- POINT ZM (30 10 40 300) +- POINT ZM (40 20 60 800) +- null +- POINT ZM EMPTY + +linestring-zm: +- LINESTRING ZM (30 10 40 300, 10 30 40 300, 40 40 80 1600) +- LINESTRING ZM (40 20 60 800, 20 40 60 800, 50 50 100 2500) +- null +- LINESTRING ZM EMPTY + +polygon-zm: +- POLYGON ZM ((30 10 40 300, 40 40 80 1600, 20 40 60 800, 10 20 30 200, 30 10 40 300)) +- POLYGON ZM ((35 10 45 350, 45 45 90 2025, 15 40 55 600, 10 20 30 200, 35 10 45 350), + (20 30 50 600, 35 35 70 1225, 30 20 50 600, 20 30 50 600)) +- null +- POLYGON ZM EMPTY +multipoint-zm: +- MULTIPOINT ZM ((30 10 40 300)) +- MULTIPOINT ZM ((10 40 50 400), (40 30 70 1200), (20 20 40 400), (30 10 40 300)) +- null +- MULTIPOINT ZM EMPTY + +multilinestring-zm: +- MULTILINESTRING ZM ((30 10 40 300, 10 30 40 300, 40 40 80 1600)) +- MULTILINESTRING ZM ((10 10 20 100, 20 20 40 400, 10 40 50 400), (40 40 80 1600, + 30 30 60 900, 40 20 60 800, 30 10 40 300)) +- null +- MULTILINESTRING ZM EMPTY + +multipolygon-zm: +- MULTIPOLYGON ZM (((30 10 40 300, 40 40 80 1600, 20 40 60 800, 10 20 30 200, 30 10 + 40 300))) +- MULTIPOLYGON ZM (((30 20 50 600, 45 40 85 1800, 10 40 50 400, 30 20 50 600)), ((15 + 5 20 75, 40 10 50 400, 10 20 30 200, 5 10 15 50, 15 5 20 75))) +- MULTIPOLYGON ZM (((40 40 80 1600, 20 45 65 900, 45 30 75 1350, 40 40 80 1600)), + ((20 35 55 700, 10 30 40 300, 10 10 20 100, 30 5 35 150, 45 20 65 900, 20 35 55 + 700), (30 20 50 600, 20 15 35 300, 20 25 45 500, 30 20 50 600))) +- null +- MULTIPOLYGON ZM EMPTY + +geometrycollection-zm: +- GEOMETRYCOLLECTION ZM (POINT ZM (30 10 40 300)) +- GEOMETRYCOLLECTION ZM (LINESTRING ZM (30 10 40 300, 10 30 40 300, 40 40 80 1600)) +- GEOMETRYCOLLECTION ZM (POLYGON ZM ((30 10 40 300, 40 40 80 1600, 20 40 60 800, 10 + 20 30 200, 30 10 40 300))) +- GEOMETRYCOLLECTION ZM (MULTIPOINT ZM ((30 10 40 300))) +- GEOMETRYCOLLECTION ZM (MULTILINESTRING ZM ((30 10 40 300, 10 30 40 300, 40 40 80 + 1600))) +- GEOMETRYCOLLECTION ZM (MULTIPOLYGON ZM (((30 10 40 300, 40 40 80 1600, 20 40 60 + 800, 10 20 30 200, 30 10 40 300)))) +- GEOMETRYCOLLECTION ZM (POINT ZM (30 10 40 300), LINESTRING ZM (30 10 40 300, 10 + 30 40 300, 40 40 80 1600), POLYGON ZM ((30 10 40 300, 40 40 80 1600, 20 40 60 800, + 10 20 30 200, 30 10 40 300)), MULTIPOINT ZM ((30 10 40 300)), MULTILINESTRING ZM + ((30 10 40 300, 10 30 40 300, 40 40 80 1600)), MULTIPOLYGON ZM (((30 10 40 300, + 40 40 80 1600, 20 40 60 800, 10 20 30 200, 30 10 40 300)))) +- null +- GEOMETRYCOLLECTION ZM EMPTY