Fixed #30678 -- Added support for GDAL 3.

This commit is contained in:
Claude Paroz 2020-05-08 11:28:52 +02:00 committed by Mariusz Felisiak
parent 0668164b4a
commit 58f1b07e49
11 changed files with 140 additions and 94 deletions

View File

@ -37,11 +37,13 @@ from django.contrib.gis.gdal.libgdal import (
GDAL_VERSION, gdal_full_version, gdal_version,
)
from django.contrib.gis.gdal.raster.source import GDALRaster
from django.contrib.gis.gdal.srs import CoordTransform, SpatialReference
from django.contrib.gis.gdal.srs import (
AxisOrder, CoordTransform, SpatialReference,
)
__all__ = (
'Driver', 'DataSource', 'CoordTransform', 'Envelope', 'GDALException',
'GDALRaster', 'GDAL_VERSION', 'OGRGeometry', 'OGRGeomType',
'SpatialReference', 'SRSException', 'check_err', 'gdal_version',
'gdal_full_version',
'AxisOrder', 'Driver', 'DataSource', 'CoordTransform', 'Envelope',
'GDALException', 'GDALRaster', 'GDAL_VERSION', 'OGRGeometry',
'OGRGeomType', 'SpatialReference', 'SRSException', 'check_err',
'gdal_version', 'gdal_full_version',
)

View File

@ -1,6 +1,6 @@
from ctypes import POINTER, c_char_p, c_int, c_void_p
from django.contrib.gis.gdal.libgdal import lgdal, std_call
from django.contrib.gis.gdal.libgdal import GDAL_VERSION, lgdal, std_call
from django.contrib.gis.gdal.prototypes.generation import (
const_string_output, double_output, int_output, srs_output, string_output,
void_output,
@ -31,6 +31,9 @@ release_srs = void_output(lgdal.OSRRelease, [c_void_p], errcheck=False)
destroy_srs = void_output(std_call('OSRDestroySpatialReference'), [c_void_p], errcheck=False)
srs_validate = void_output(lgdal.OSRValidate, [c_void_p])
if GDAL_VERSION >= (3, 0):
set_axis_strategy = void_output(lgdal.OSRSetAxisMappingStrategy, [c_void_p, c_int])
# Getting the semi_major, semi_minor, and flattening functions.
semi_major = srs_double(lgdal.OSRGetSemiMajor)
semi_minor = srs_double(lgdal.OSRGetSemiMinor)

View File

@ -27,13 +27,20 @@
NAD83 / Texas South Central
"""
from ctypes import byref, c_char_p, c_int
from enum import IntEnum
from django.contrib.gis.gdal.base import GDALBase
from django.contrib.gis.gdal.error import SRSException
from django.contrib.gis.gdal.libgdal import GDAL_VERSION
from django.contrib.gis.gdal.prototypes import srs as capi
from django.utils.encoding import force_bytes, force_str
class AxisOrder(IntEnum):
TRADITIONAL = 0
AUTHORITY = 1
class SpatialReference(GDALBase):
"""
A wrapper for the OGRSpatialReference object. According to the GDAL Web site,
@ -42,17 +49,25 @@ class SpatialReference(GDALBase):
"""
destructor = capi.release_srs
def __init__(self, srs_input='', srs_type='user'):
def __init__(self, srs_input='', srs_type='user', axis_order=None):
"""
Create a GDAL OSR Spatial Reference object from the given input.
The input may be string of OGC Well Known Text (WKT), an integer
EPSG code, a PROJ.4 string, and/or a projection "well known" shorthand
string (one of 'WGS84', 'WGS72', 'NAD27', 'NAD83').
"""
if not isinstance(axis_order, (type(None), AxisOrder)):
raise ValueError(
'SpatialReference.axis_order must be an AxisOrder instance.'
)
self.axis_order = axis_order or AxisOrder.TRADITIONAL
if srs_type == 'wkt':
self.ptr = capi.new_srs(c_char_p(b''))
self.import_wkt(srs_input)
if self.axis_order == AxisOrder.TRADITIONAL and GDAL_VERSION >= (3, 0):
capi.set_axis_strategy(self.ptr, self.axis_order)
elif self.axis_order != AxisOrder.TRADITIONAL and GDAL_VERSION < (3, 0):
raise ValueError('%s is not supported in GDAL < 3.0.' % self.axis_order)
return
elif isinstance(srs_input, str):
try:
@ -85,6 +100,10 @@ class SpatialReference(GDALBase):
else:
self.ptr = srs
if self.axis_order == AxisOrder.TRADITIONAL and GDAL_VERSION >= (3, 0):
capi.set_axis_strategy(self.ptr, self.axis_order)
elif self.axis_order != AxisOrder.TRADITIONAL and GDAL_VERSION < (3, 0):
raise ValueError('%s is not supported in GDAL < 3.0.' % self.axis_order)
# Importing from either the user input string or an integer SRID.
if srs_type == 'user':
self.import_user_input(srs_input)
@ -143,7 +162,7 @@ class SpatialReference(GDALBase):
def clone(self):
"Return a clone of this SpatialReference object."
return SpatialReference(capi.clone_srs(self.ptr))
return SpatialReference(capi.clone_srs(self.ptr), axis_order=self.axis_order)
def from_esri(self):
"Morph this SpatialReference from ESRI's format to EPSG."

View File

@ -434,7 +434,7 @@ class GEOSGeometryBase(GEOSBase):
if self.srid:
try:
return gdal.SpatialReference(self.srid)
except gdal.SRSException:
except (gdal.GDALException, gdal.SRSException):
pass
return None

View File

@ -9,8 +9,8 @@ geospatial libraries:
Program Description Required Supported Versions
======================== ==================================== ================================ ===================================
:doc:`GEOS <../geos>` Geometry Engine Open Source Yes 3.7, 3.6, 3.5
`PROJ.4`_ Cartographic Projections library Yes (PostgreSQL and SQLite only) 5.2, 5.1, 5.0, 4.x
:doc:`GDAL <../gdal>` Geospatial Data Abstraction Library Yes 2.4, 2.3, 2.2, 2.1, 2.0
`PROJ.4`_ Cartographic Projections library Yes (PostgreSQL and SQLite only) 6.3, 6.2, 6.1, 6.0, 5.x, 4.x
:doc:`GDAL <../gdal>` Geospatial Data Abstraction Library Yes 3.0, 2.4, 2.3, 2.2, 2.1, 2.0
:doc:`GeoIP <../geoip2>` IP-based geolocation library No 2
`PostGIS`__ Spatial extensions for PostgreSQL Yes (PostgreSQL only) 3.0, 2.5, 2.4, 2.3, 2.2
`SpatiaLite`__ Spatial extensions for SQLite Yes (SQLite only) 4.3
@ -29,6 +29,7 @@ totally fine with GeoDjango. Your mileage may vary.
GDAL 2.2.0 2017-05
GDAL 2.3.0 2018-05
GDAL 2.4.0 2018-12
GDAL 3.0.0 2019-05
PostGIS 2.2.0 2015-10-17
PostGIS 2.3.0 2016-09-26
PostGIS 2.4.0 2017-09-30

View File

@ -145,7 +145,7 @@ Minor features
* Added the :class:`~django.contrib.gis.db.models.functions.AsWKB` and
:class:`~django.contrib.gis.db.models.functions.AsWKT` functions.
* Added support for PostGIS 3.
* Added support for PostGIS 3 and GDAL 3.
:mod:`django.contrib.humanize`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -146,7 +146,11 @@ class GDALRasterTests(SimpleTestCase):
# Reload newly created raster from file
restored_raster = GDALRaster(rstfile.name)
self.assertEqual(restored_raster.srs.wkt, self.rs.srs.wkt)
# Presence of TOWGS84 depend on GDAL/Proj versions.
self.assertEqual(
restored_raster.srs.wkt.replace('TOWGS84[0,0,0,0,0,0,0],', ''),
self.rs.srs.wkt.replace('TOWGS84[0,0,0,0,0,0,0],', '')
)
self.assertEqual(restored_raster.geotransform, self.rs.geotransform)
if numpy:
numpy.testing.assert_equal(
@ -322,54 +326,33 @@ class GDALRasterTests(SimpleTestCase):
with self.assertRaisesMessage(ValueError, msg):
self.rs.info
return
gdalinfo = """
Driver: GTiff/GeoTIFF
Files: {}
Size is 163, 174
Coordinate System is:
PROJCS["NAD83 / Florida GDL Albers",
GEOGCS["NAD83",
DATUM["North_American_Datum_1983",
SPHEROID["GRS 1980",6378137,298.257222101,
AUTHORITY["EPSG","7019"]],
TOWGS84[0,0,0,0,0,0,0],
AUTHORITY["EPSG","6269"]],
PRIMEM["Greenwich",0,
AUTHORITY["EPSG","8901"]],
UNIT["degree",0.0174532925199433,
AUTHORITY["EPSG","9122"]],
AUTHORITY["EPSG","4269"]],
PROJECTION["Albers_Conic_Equal_Area"],
PARAMETER["standard_parallel_1",24],
PARAMETER["standard_parallel_2",31.5],
PARAMETER["latitude_of_center",24],
PARAMETER["longitude_of_center",-84],
PARAMETER["false_easting",400000],
PARAMETER["false_northing",0],
UNIT["metre",1,
AUTHORITY["EPSG","9001"]],
AXIS["X",EAST],
AXIS["Y",NORTH],
AUTHORITY["EPSG","3086"]]
Origin = (511700.468070655711927,435103.377123198588379)
Pixel Size = (100.000000000000000,-100.000000000000000)
Metadata:
AREA_OR_POINT=Area
Image Structure Metadata:
INTERLEAVE=BAND
Corner Coordinates:
Upper Left ( 511700.468, 435103.377) ( 82d51'46.16"W, 27d55' 1.53"N)
Lower Left ( 511700.468, 417703.377) ( 82d51'52.04"W, 27d45'37.50"N)
Upper Right ( 528000.468, 435103.377) ( 82d41'48.81"W, 27d54'56.30"N)
Lower Right ( 528000.468, 417703.377) ( 82d41'55.54"W, 27d45'32.28"N)
Center ( 519850.468, 426403.377) ( 82d46'50.64"W, 27d50'16.99"N)
Band 1 Block=163x50 Type=Byte, ColorInterp=Gray
NoData Value=15
""".format(self.rs_path)
infos = self.rs.info
# Data
info_dyn = [line.strip() for line in self.rs.info.split('\n') if line.strip() != '']
info_ref = [line.strip() for line in gdalinfo.split('\n') if line.strip() != '']
self.assertEqual(info_dyn, info_ref)
info_lines = [line.strip() for line in infos.split('\n') if line.strip() != '']
for line in [
'Driver: GTiff/GeoTIFF',
'Files: {}'.format(self.rs_path),
'Size is 163, 174',
'Origin = (511700.468070655711927,435103.377123198588379)',
'Pixel Size = (100.000000000000000,-100.000000000000000)',
'Metadata:',
'AREA_OR_POINT=Area',
'Image Structure Metadata:',
'INTERLEAVE=BAND',
'Band 1 Block=163x50 Type=Byte, ColorInterp=Gray',
'NoData Value=15'
]:
self.assertIn(line, info_lines)
for line in [
r'Upper Left \( 511700.468, 435103.377\) \( 82d51\'46.1\d"W, 27d55\' 1.5\d"N\)',
r'Lower Left \( 511700.468, 417703.377\) \( 82d51\'52.0\d"W, 27d45\'37.5\d"N\)',
r'Upper Right \( 528000.468, 435103.377\) \( 82d41\'48.8\d"W, 27d54\'56.3\d"N\)',
r'Lower Right \( 528000.468, 417703.377\) \( 82d41\'55.5\d"W, 27d45\'32.2\d"N\)',
r'Center \( 519850.468, 426403.377\) \( 82d46\'50.6\d"W, 27d50\'16.9\d"N\)',
]:
self.assertRegex(infos, line)
# CRS (skip the name because string depends on the GDAL/Proj versions).
self.assertIn("NAD83 / Florida GDL Albers", infos)
def test_compressed_file_based_raster_creation(self):
rstfile = tempfile.NamedTemporaryFile(suffix='.tif')

View File

@ -1,8 +1,11 @@
import unittest
from unittest import skipIf
from django.contrib.gis.gdal import (
CoordTransform, GDALException, SpatialReference, SRSException,
GDAL_VERSION, AxisOrder, CoordTransform, GDALException, SpatialReference,
SRSException,
)
from django.contrib.gis.geos import GEOSGeometry
from django.test import SimpleTestCase
class TestSRS:
@ -20,7 +23,8 @@ srlist = (
'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,'
'AUTHORITY["EPSG","7030"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6326"]],'
'PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",'
'0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]',
'0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],'
'AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]]',
epsg=4326, projected=False, geographic=True, local=False,
lin_name='unknown', ang_name='degree', lin_units=1.0, ang_units=0.0174532925199,
auth={'GEOGCS': ('EPSG', '4326'), 'spheroid': ('EPSG', '7030')},
@ -30,14 +34,14 @@ srlist = (
'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",'
'SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],'
'AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],'
'UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],'
'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],'
'AUTHORITY["EPSG","4269"]],PROJECTION["Lambert_Conformal_Conic_2SP"],'
'PARAMETER["standard_parallel_1",30.28333333333333],'
'PARAMETER["standard_parallel_2",28.38333333333333],'
'PARAMETER["latitude_of_origin",27.83333333333333],'
'PARAMETER["standard_parallel_1",30.2833333333333],'
'PARAMETER["standard_parallel_2",28.3833333333333],'
'PARAMETER["latitude_of_origin",27.8333333333333],'
'PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],'
'PARAMETER["false_northing",4000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],'
'AUTHORITY["EPSG","32140"]]',
'AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32140"]]',
epsg=32140, projected=True, geographic=False, local=False,
lin_name='metre', ang_name='degree', lin_units=1.0, ang_units=0.0174532925199,
auth={'PROJCS': ('EPSG', '32140'), 'spheroid': ('EPSG', '7019'), 'unit': ('EPSG', '9001')},
@ -48,25 +52,27 @@ srlist = (
),
),
TestSRS(
'PROJCS["NAD_1983_StatePlane_Texas_South_Central_FIPS_4204_Feet",'
'GEOGCS["GCS_North_American_1983",DATUM["North_American_Datum_1983",'
'SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],'
'PROJCS["NAD83 / Texas South Central (ftUS)",'
'GEOGCS["NAD83",DATUM["North_American_Datum_1983",'
'SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],'
'PRIMEM["Greenwich",0],'
'UNIT["Degree",0.0174532925199433]],PROJECTION["Lambert_Conformal_Conic_2SP"],'
'PARAMETER["False_Easting",1968500.0],PARAMETER["False_Northing",13123333.33333333],'
'PARAMETER["Central_Meridian",-99.0],PARAMETER["Standard_Parallel_1",28.38333333333333],'
'PARAMETER["Standard_Parallel_2",30.28333333333334],PARAMETER["Latitude_Of_Origin",27.83333333333333],'
'UNIT["Foot_US",0.3048006096012192]]',
'PARAMETER["false_easting",1968500],PARAMETER["false_northing",13123333.3333333],'
'PARAMETER["central_meridian",-99],PARAMETER["standard_parallel_1",28.3833333333333],'
'PARAMETER["standard_parallel_2",30.2833333333333],PARAMETER["latitude_of_origin",27.8333333333333],'
'UNIT["US survey foot",0.304800609601219],AXIS["Easting",EAST],AXIS["Northing",NORTH]]',
epsg=None, projected=True, geographic=False, local=False,
lin_name='Foot_US', ang_name='Degree', lin_units=0.3048006096012192, ang_units=0.0174532925199,
lin_name='US survey foot', ang_name='Degree', lin_units=0.3048006096012192, ang_units=0.0174532925199,
auth={'PROJCS': (None, None)},
attr=(('PROJCS|GeOgCs|spheroid', 'GRS_1980'), (('projcs', 9), 'UNIT'), (('projcs', 11), None),),
attr=(('PROJCS|GeOgCs|spheroid', 'GRS 1980'), (('projcs', 9), 'UNIT'), (('projcs', 11), 'AXIS'),),
),
# This is really ESRI format, not WKT -- but the import should work the same
TestSRS(
'LOCAL_CS["Non-Earth (Meter)",LOCAL_DATUM["Local Datum",0],UNIT["Meter",1.0],AXIS["X",EAST],AXIS["Y",NORTH]]',
'LOCAL_CS["Non-Earth (Meter)",LOCAL_DATUM["Local Datum",32767],'
'UNIT["Meter",1],AXIS["X",EAST],AXIS["Y",NORTH]]',
esri=True, epsg=None, projected=False, geographic=False, local=True,
lin_name='Meter', ang_name='degree', lin_units=1.0, ang_units=0.0174532925199,
attr=(('LOCAL_DATUM', 'Local Datum'), ('unit', 'Meter')),
attr=(('LOCAL_DATUM', 'Local Datum'),),
),
)
@ -146,7 +152,7 @@ bad_srlist = (
)
class SpatialRefTest(unittest.TestCase):
class SpatialRefTest(SimpleTestCase):
def test01_wkt(self):
"Testing initialization on valid OGC WKT."
@ -168,7 +174,11 @@ class SpatialRefTest(unittest.TestCase):
"Testing getting the WKT."
for s in srlist:
srs = SpatialReference(s.wkt)
self.assertEqual(s.wkt, srs.wkt)
# GDAL 3 strips UNIT part in the last occurrence.
self.assertEqual(
s.wkt.replace(',UNIT["Meter",1]', ''),
srs.wkt.replace(',UNIT["Meter",1]', ''),
)
def test04_proj(self):
"Test PROJ.4 import and export."
@ -264,11 +274,11 @@ class SpatialRefTest(unittest.TestCase):
'GEOGCS["DHDN",DATUM["Deutsches_Hauptdreiecksnetz",'
'SPHEROID["Bessel 1841",6377397.155,299.1528128,AUTHORITY["EPSG","7004"]],AUTHORITY["EPSG","6314"]],'
'PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],'
'UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],'
'AUTHORITY["EPSG","4314"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],'
'PROJECTION["Cassini_Soldner"],PARAMETER["latitude_of_origin",50.66738711],'
'PARAMETER["central_meridian",6.28935703],PARAMETER["false_easting",0],'
'PARAMETER["false_northing",0],AUTHORITY["mj10777.de","187939"],AXIS["x",NORTH],AXIS["y",EAST]]'
'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],'
'AUTHORITY["EPSG","4314"]],PROJECTION["Cassini_Soldner"],'
'PARAMETER["latitude_of_origin",50.66738711],PARAMETER["central_meridian",6.28935703],'
'PARAMETER["false_easting",0],PARAMETER["false_northing",0],'
'UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",NORTH],AXIS["Y",EAST],AUTHORITY["mj10777.de","187939"]]'
)
srs = SpatialReference(wkt)
srs_list = [srs, srs.clone()]
@ -279,3 +289,31 @@ class SpatialRefTest(unittest.TestCase):
self.assertEqual(srs.wkt, wkt)
self.assertIn('Langschoß', srs.pretty_wkt)
self.assertIn('Langschoß', srs.xml)
@skipIf(GDAL_VERSION < (3, 0), 'GDAL >= 3.0 is required')
def test_axis_order(self):
wgs84_trad = SpatialReference(4326, axis_order=AxisOrder.TRADITIONAL)
wgs84_auth = SpatialReference(4326, axis_order=AxisOrder.AUTHORITY)
# Coordinate interpretation may depend on the srs axis predicate.
pt = GEOSGeometry('POINT (992385.4472045 481455.4944650)', 2774)
pt_trad = pt.transform(wgs84_trad, clone=True)
self.assertAlmostEqual(pt_trad.x, -104.609, 3)
self.assertAlmostEqual(pt_trad.y, 38.255, 3)
pt_auth = pt.transform(wgs84_auth, clone=True)
self.assertAlmostEqual(pt_auth.x, 38.255, 3)
self.assertAlmostEqual(pt_auth.y, -104.609, 3)
# clone() preserves the axis order.
pt_auth = pt.transform(wgs84_auth.clone(), clone=True)
self.assertAlmostEqual(pt_auth.x, 38.255, 3)
self.assertAlmostEqual(pt_auth.y, -104.609, 3)
def test_axis_order_invalid(self):
msg = 'SpatialReference.axis_order must be an AxisOrder instance.'
with self.assertRaisesMessage(ValueError, msg):
SpatialReference(4326, axis_order='other')
@skipIf(GDAL_VERSION > (3, 0), "GDAL < 3.0 doesn't support authority.")
def test_axis_order_non_traditional_invalid(self):
msg = 'AxisOrder.AUTHORITY is not supported in GDAL < 3.0.'
with self.assertRaisesMessage(ValueError, msg):
SpatialReference(4326, axis_order=AxisOrder.AUTHORITY)

View File

@ -828,8 +828,8 @@ class GEOSTest(SimpleTestCase, TestDataMixin):
gdal.SpatialReference(4326))
new_pnt = pnt.transform(c2w, clone=True)
self.assertEqual(new_pnt.srid, 4326)
self.assertAlmostEqual(new_pnt.x, 1, 3)
self.assertAlmostEqual(new_pnt.y, 2, 3)
self.assertAlmostEqual(new_pnt.x, 1, 1)
self.assertAlmostEqual(new_pnt.y, 2, 1)
def test_mutable_geometries(self):
"Testing the mutability of Polygons and Geometry Collections."

View File

@ -28,7 +28,7 @@ class GeometryFieldTest(SimpleTestCase):
# Making the field in a different SRID from that of the geometry, and
# asserting it transforms.
fld = forms.GeometryField(srid=32140)
tol = 0.0000001
tol = 0.0001
xform_geom = GEOSGeometry('POINT (951640.547328465 4219369.26171664)', srid=32140)
# The cleaned geometry is transformed to 32140 (the widget map_srid is 3857).
cleaned_geom = fld.clean('SRID=3857;POINT (-10615777.40976205 3473169.895707852)')

View File

@ -11,7 +11,7 @@ test_srs = ({
# Only the beginning, because there are differences depending on installed libs
'srtext': 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84"',
# +ellps=WGS84 has been removed in the 4326 proj string in proj-4.8
'proj4_re': r'\+proj=longlat (\+ellps=WGS84 )?(\+datum=WGS84 |\+towgs84=0,0,0,0,0,0,0 )\+no_defs ',
'proj4_re': r'\+proj=longlat (\+ellps=WGS84 )?(\+datum=WGS84 |\+towgs84=0,0,0,0,0,0,0 )\+no_defs ?',
'spheroid': 'WGS 84', 'name': 'WGS 84',
'geographic': True, 'projected': False, 'spatialite': True,
# From proj's "cs2cs -le" and Wikipedia (semi-minor only)
@ -37,9 +37,9 @@ test_srs = ({
'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",'
'DATUM["North_American_Datum_1983",SPHEROID["GRS 1980"'
),
'proj4_re': r'\+proj=lcc \+lat_1=30.28333333333333 \+lat_2=28.38333333333333 \+lat_0=27.83333333333333 '
r'\+lon_0=-99 \+x_0=600000 \+y_0=4000000 (\+ellps=GRS80 )?'
r'(\+datum=NAD83 |\+towgs84=0,0,0,0,0,0,0 )?\+units=m \+no_defs ',
'proj4_re': r'\+proj=lcc (\+lat_1=30.28333333333333? |\+lat_2=28.38333333333333? |\+lat_0=27.83333333333333? |'
r'\+lon_0=-99 ){4}\+x_0=600000 \+y_0=4000000 (\+ellps=GRS80 )?'
r'(\+datum=NAD83 |\+towgs84=0,0,0,0,0,0,0 )?\+units=m \+no_defs ?',
'spheroid': 'GRS 1980', 'name': 'NAD83 / Texas South Central',
'geographic': False, 'projected': True, 'spatialite': False,
# From proj's "cs2cs -le" and Wikipedia (semi-minor only)