1import concurrent.futures
2import json
3
4import numpy
5import pytest
6
7import pyproj
8from pyproj import CRS
9from pyproj._crs import AuthorityMatchInfo
10from pyproj.crs import (
11    CoordinateOperation,
12    CoordinateSystem,
13    Datum,
14    Ellipsoid,
15    PrimeMeridian,
16)
17from pyproj.crs.enums import CoordinateOperationType, DatumType
18from pyproj.enums import ProjVersion, WktVersion
19from pyproj.exceptions import CRSError
20from pyproj.transformer import TransformerGroup
21from test.conftest import (
22    HAYFORD_ELLIPSOID_NAME,
23    PROJ_GTE_8,
24    assert_can_pickle,
25    get_wgs84_datum_name,
26    grids_available,
27)
28
29
30class CustomCRS(object):
31    def to_wkt(self):
32        return CRS.from_epsg(4326).to_wkt()
33
34
35def test_from_proj4_json():
36    json_str = '{"proj": "longlat", "ellps": "WGS84", "datum": "WGS84"}'
37    proj = CRS.from_string(json_str)
38    with pytest.warns(UserWarning):
39        assert proj.to_proj4(4) == "+proj=longlat +datum=WGS84 +no_defs +type=crs"
40        assert proj.to_proj4(5) == "+proj=longlat +datum=WGS84 +no_defs +type=crs"
41    # Test with invalid JSON code
42    with pytest.raises(CRSError):
43        assert CRS.from_string("{foo: bar}")
44
45
46def test_from_proj4():
47    proj = CRS.from_proj4("+proj=longlat +datum=WGS84 +no_defs +type=crs")
48    with pytest.warns(UserWarning):
49        assert proj.to_proj4() == "+proj=longlat +datum=WGS84 +no_defs +type=crs"
50
51
52def test_from_proj4__invalid():
53    # Test with invalid JSON code
54    with pytest.raises(CRSError):
55        assert CRS.from_proj4(CRS(3857).to_wkt())
56
57
58def test_from_epsg():
59    proj = CRS.from_epsg(4326)
60    assert proj.to_epsg() == 4326
61
62    # Test with invalid EPSG code
63    with pytest.raises(CRSError):
64        assert CRS.from_epsg(0)
65
66
67def test_from_epsg_string():
68    proj = CRS.from_string("epsg:4326")
69    assert proj.to_epsg() == 4326
70
71    # Test with invalid EPSG code
72    with pytest.raises(CRSError):
73        assert CRS.from_string("epsg:xyz")
74
75
76def test_from_string():
77    wgs84_crs = CRS.from_string("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs")
78    with pytest.warns(UserWarning):
79        assert wgs84_crs.to_proj4() == "+proj=longlat +datum=WGS84 +no_defs +type=crs"
80    # Make sure this doesn't get handled using the from_epsg()
81    # even though 'epsg' is in the string
82    with pytest.warns(FutureWarning):
83        epsg_init_crs = CRS.from_string("+init=epsg:26911 +units=m +no_defs=True")
84    with pytest.warns(UserWarning):
85        assert (
86            epsg_init_crs.to_proj4()
87            == "+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs +type=crs"
88        )
89
90
91def test_from_string__invalid():
92    with pytest.raises(CRSError, match="CRS input is not a string"):
93        CRS.from_string(4326)
94
95
96def test_initialize_projparams_with_kwargs():
97    crs_mixed_args = CRS("+proj=utm +zone=10", ellps="WGS84")
98    crs_positional = CRS("+proj=utm +zone=10 +ellps=WGS84")
99    assert crs_mixed_args.is_exact_same(crs_positional)
100
101
102def test_bare_parameters():
103    """Make sure that bare parameters (e.g., no_defs) are handled properly,
104    even if they come in with key=True.  This covers interaction with pyproj,
105    which makes presents bare parameters as key=<bool>."""
106
107    # Example produced by pyproj
108    proj = CRS.from_string(
109        "+proj=lcc +lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True "
110        "+x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0"
111    )
112    with pytest.warns(UserWarning):
113        assert "+no_defs" in proj.to_proj4(4)
114
115    # TODO: THIS DOES NOT WORK
116    proj = CRS.from_string(
117        "+lon_0=-95 +ellps=GRS80 +proj=lcc +y_0=0 +no_defs=False "
118        "+x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0"
119    )
120    # assert "+no_defs" not in proj.to_proj4(4)
121
122
123def test_is_geographic():
124    assert CRS("EPSG:4326").is_geographic is True
125    assert CRS("EPSG:3857").is_geographic is False
126
127    wgs84_crs = CRS.from_string("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs")
128    assert wgs84_crs.is_geographic is True
129
130    nad27_crs = CRS.from_string("+proj=longlat +ellps=clrk66 +datum=NAD27 +no_defs")
131    assert nad27_crs.is_geographic is True
132
133    lcc_crs = CRS.from_string(
134        "+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 "
135        "+units=m +lat_2=77 +lat_1=49 +lat_0=0"
136    )
137    assert lcc_crs.is_geographic is False
138
139
140def test_is_projected():
141    assert CRS("EPSG:3857").is_projected is True
142
143    lcc_crs = CRS.from_string(
144        "+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 "
145        "+units=m +lat_2=77 +lat_1=49 +lat_0=0"
146    )
147    assert CRS.from_user_input(lcc_crs).is_projected is True
148
149    wgs84_crs = CRS.from_string("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs")
150    assert CRS.from_user_input(wgs84_crs).is_projected is False
151
152
153def test_is_compound():
154    assert CRS("EPSG:4326+5773").is_compound
155    assert not CRS("EPSG:4326").is_compound
156
157
158def test_is_same_crs():
159    crs1 = CRS("urn:ogc:def:crs:OGC::CRS84")
160    crs2 = CRS("EPSG:3857")
161
162    assert crs1 == crs1
163    assert crs1 != crs2
164
165    wgs84_crs = CRS.from_string("+proj=longlat +ellps=WGS84 +datum=WGS84")
166    assert crs1 == wgs84_crs
167
168    # Make sure that same projection with different parameter are not equal
169    lcc_crs1 = CRS.from_string(
170        "+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc "
171        "+x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0"
172    )
173    lcc_crs2 = CRS.from_string(
174        "+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc "
175        "+x_0=0 +units=m +lat_2=77 +lat_1=45 +lat_0=0"
176    )
177    assert lcc_crs1 != lcc_crs2
178
179
180def test_to_proj4():
181    with pytest.warns(UserWarning):
182        assert (
183            CRS("EPSG:4326").to_proj4(4)
184            == "+proj=longlat +datum=WGS84 +no_defs +type=crs"
185        )
186
187
188def test_empty_json():
189    with pytest.raises(CRSError):
190        CRS.from_string("{}")
191    with pytest.raises(CRSError):
192        CRS.from_string("[]")
193    with pytest.raises(CRSError):
194        CRS.from_string("")
195
196
197def test_has_wkt_property():
198    with pytest.warns(FutureWarning):
199        assert (
200            CRS({"init": "EPSG:4326"})
201            .to_wkt("WKT1_GDAL")
202            .startswith('GEOGCS["WGS 84",DATUM')
203        )
204
205
206def test_to_wkt_pretty():
207    crs = CRS.from_epsg(4326)
208    assert "\n" in crs.to_wkt(pretty=True)
209    assert "\n" not in crs.to_wkt()
210
211
212def test_repr():
213    with pytest.warns(FutureWarning):
214        assert repr(CRS({"init": "EPSG:4326"})) == (
215            "<Geographic 2D CRS: +init=epsg:4326 +type=crs>\n"
216            "Name: WGS 84\n"
217            "Axis Info [ellipsoidal]:\n"
218            "- lon[east]: Longitude (degree)\n"
219            "- lat[north]: Latitude (degree)\n"
220            "Area of Use:\n"
221            "- name: World.\n"
222            "- bounds: (-180.0, -90.0, 180.0, 90.0)\n"
223            f"Datum: {get_wgs84_datum_name()}\n"
224            "- Ellipsoid: WGS 84\n"
225            "- Prime Meridian: Greenwich\n"
226        )
227
228
229def test_repr__long():
230    with pytest.warns(FutureWarning):
231        if PROJ_GTE_8:
232            wkt_str = 'GEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1'
233        else:
234            wkt_str = 'GEOGCRS["WGS 84",DATUM["World Geodetic System 1984'
235        assert repr(CRS(CRS({"init": "EPSG:4326"}).to_wkt())) == (
236            f"<Geographic 2D CRS: {wkt_str} ...>\n"
237            "Name: WGS 84\n"
238            "Axis Info [ellipsoidal]:\n"
239            "- lon[east]: Longitude (degree)\n"
240            "- lat[north]: Latitude (degree)\n"
241            "Area of Use:\n"
242            "- name: World.\n"
243            "- bounds: (-180.0, -90.0, 180.0, 90.0)\n"
244            f"Datum: {get_wgs84_datum_name()}\n"
245            "- Ellipsoid: WGS 84\n"
246            "- Prime Meridian: Greenwich\n"
247        )
248
249
250def test_repr_epsg():
251    assert repr(CRS(CRS("EPSG:4326").to_wkt())) == (
252        "<Geographic 2D CRS: EPSG:4326>\n"
253        "Name: WGS 84\n"
254        "Axis Info [ellipsoidal]:\n"
255        "- Lat[north]: Geodetic latitude (degree)\n"
256        "- Lon[east]: Geodetic longitude (degree)\n"
257        "Area of Use:\n"
258        "- name: World.\n"
259        "- bounds: (-180.0, -90.0, 180.0, 90.0)\n"
260        f"Datum: {get_wgs84_datum_name()}\n"
261        "- Ellipsoid: WGS 84\n"
262        "- Prime Meridian: Greenwich\n"
263    )
264
265
266def test_repr__undefined():
267    assert repr(
268        CRS(
269            "+proj=merc +a=6378137.0 +b=6378137.0 +nadgrids=@null"
270            " +lon_0=0.0 +x_0=0.0 +y_0=0.0 +units=m +no_defs"
271        )
272    ) == (
273        "<Bound CRS: +proj=merc +a=6378137.0 +b=6378137.0 +nadgrids=@nu ...>\n"
274        "Name: unknown\n"
275        "Axis Info [cartesian]:\n"
276        "- E[east]: Easting (metre)\n"
277        "- N[north]: Northing (metre)\n"
278        "Area of Use:\n"
279        "- undefined\n"
280        "Coordinate Operation:\n"
281        "- name: unknown to WGS84\n"
282        "- method: NTv2\n"
283        "Datum: unknown\n"
284        "- Ellipsoid: unknown\n"
285        "- Prime Meridian: Greenwich\n"
286        "Source CRS: unknown\n"
287    )
288
289
290def test_repr_compound():
291    assert repr(CRS.from_epsg(3901)) == (
292        "<Compound CRS: EPSG:3901>\n"
293        "Name: KKJ / Finland Uniform Coordinate System + N60 height\n"
294        "Axis Info [cartesian|vertical]:\n"
295        "- X[north]: Northing (metre)\n"
296        "- Y[east]: Easting (metre)\n"
297        "- H[up]: Gravity-related height (metre)\n"
298        "Area of Use:\n"
299        "- name: Finland - onshore.\n"
300        "- bounds: (19.24, 59.75, 31.59, 70.09)\n"
301        "Datum: Kartastokoordinaattijarjestelma (1966)\n"
302        "- Ellipsoid: International 1924\n"
303        "- Prime Meridian: Greenwich\n"
304        "Sub CRS:\n"
305        "- KKJ / Finland Uniform Coordinate System\n"
306        "- N60 height\n"
307    )
308
309
310def test_axis_info_compound():
311    assert [axis.direction for axis in CRS.from_epsg(3901).axis_info] == [
312        "north",
313        "east",
314        "up",
315    ]
316
317
318def test_dunder_str():
319    with pytest.warns(FutureWarning):
320        assert str(CRS({"init": "EPSG:4326"})) == CRS({"init": "EPSG:4326"}).srs
321
322
323def test_epsg():
324    with pytest.warns(FutureWarning):
325        assert CRS({"init": "EPSG:4326"}).to_epsg(20) == 4326
326        assert CRS({"init": "EPSG:4326"}).to_epsg() is None
327    assert CRS.from_user_input(4326).to_epsg() == 4326
328    assert CRS.from_epsg(4326).to_epsg() == 4326
329    assert CRS.from_user_input("epsg:4326").to_epsg() == 4326
330
331
332def test_datum():
333    datum = CRS.from_epsg(4326).datum
334    assert "\n" in repr(datum)
335    if PROJ_GTE_8:
336        datum_wkt = 'ENSEMBLE["World Geodetic System 1984 ensemble"'
337    else:
338        datum_wkt = 'DATUM["World Geodetic System 1984"'
339    assert repr(datum).startswith(datum_wkt)
340    assert datum.to_wkt().startswith(datum_wkt)
341    assert datum == datum
342    assert datum.is_exact_same(datum)
343
344
345def test_datum_horizontal():
346    assert CRS.from_epsg(5972).datum == CRS.from_epsg(25832).datum
347
348
349def test_datum_unknown():
350    crs = CRS(
351        "+proj=omerc +lat_0=-36.10360962430914 "
352        "+lonc=147.06322917270154 +alpha=-54.786229796129035 "
353        "+k=1 +x_0=0 +y_0=0 +gamma=0 +ellps=WGS84 "
354        "+towgs84=0,0,0,0,0,0,0 +units=m +no_defs"
355    )
356    assert crs.datum.name == "Unknown based on WGS84 ellipsoid"
357
358
359def test_epsg__not_found():
360    assert CRS("+proj=longlat +datum=WGS84 +no_defs +towgs84=0,0,0").to_epsg(0) is None
361    assert (
362        CRS.from_string("+proj=longlat +datum=WGS84 +no_defs +towgs84=0,0,0").to_epsg()
363        is None
364    )
365
366
367def test_epsg__no_code_available():
368    lcc_crs = CRS.from_string(
369        "+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc "
370        "+x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0"
371    )
372    assert lcc_crs.to_epsg() is None
373
374
375def test_crs_OSR_equivalence():
376    crs1 = CRS.from_string("+proj=longlat +datum=WGS84 +no_defs")
377    crs2 = CRS.from_string("+proj=latlong +datum=WGS84 +no_defs")
378    with pytest.warns(FutureWarning):
379        crs3 = CRS({"init": "EPSG:4326"})
380    assert crs1 == crs2
381    # these are not equivalent in proj.4 now as one uses degrees and the othe radians
382    assert crs1 == crs3
383
384
385def test_crs_OSR_no_equivalence():
386    crs1 = CRS.from_string("+proj=longlat +datum=WGS84 +no_defs")
387    crs2 = CRS.from_string("+proj=longlat +datum=NAD27 +no_defs")
388    assert crs1 != crs2
389
390
391def test_init_from_wkt():
392    wgs84 = CRS.from_string("+proj=longlat +datum=WGS84 +no_defs")
393    from_wkt = CRS(wgs84.to_wkt())
394    assert wgs84.to_wkt() == from_wkt.to_wkt()
395
396
397def test_init_from_wkt_invalid():
398    with pytest.raises(CRSError):
399        CRS("trash-54322")
400    with pytest.raises(CRSError):
401        CRS("")
402
403
404def test_from_wkt():
405    wgs84 = CRS.from_string("+proj=longlat +datum=WGS84 +no_defs")
406    from_wkt = CRS.from_wkt(wgs84.to_wkt())
407    assert wgs84.to_wkt() == from_wkt.to_wkt()
408
409
410def test_from_wkt_invalid():
411    with pytest.raises(CRSError), pytest.warns(UserWarning):
412        CRS.from_wkt(CRS(4326).to_proj4())
413
414
415def test_from_user_input_epsg():
416    with pytest.warns(UserWarning):
417        assert "+proj=longlat" in CRS.from_user_input("EPSG:4326").to_proj4(4)
418
419
420def test_from_esri_wkt():
421    projection_string = (
422        'PROJCS["USA_Contiguous_Albers_Equal_Area_Conic_USGS_version",'
423        'GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",'
424        'SPHEROID["GRS_1980",6378137.0,298.257222101]],'
425        'PRIMEM["Greenwich",0.0],'
426        'UNIT["Degree",0.0174532925199433]],'
427        'PROJECTION["Albers"],'
428        'PARAMETER["false_easting",0.0],'
429        'PARAMETER["false_northing",0.0],'
430        'PARAMETER["central_meridian",-96.0],'
431        'PARAMETER["standard_parallel_1",29.5],'
432        'PARAMETER["standard_parallel_2",45.5],'
433        'PARAMETER["latitude_of_origin",23.0],'
434        'UNIT["Meter",1.0],'
435        'VERTCS["NAVD_1988",'
436        'VDATUM["North_American_Vertical_Datum_1988"],'
437        'PARAMETER["Vertical_Shift",0.0],'
438        'PARAMETER["Direction",1.0],UNIT["Centimeter",0.01]]]'
439    )
440    proj_crs_str = CRS.from_string(projection_string)
441    proj_crs_wkt = CRS(projection_string)
442    with pytest.warns(UserWarning):
443        assert proj_crs_str.to_proj4() == proj_crs_wkt.to_proj4()
444        assert proj_crs_str.to_proj4(4) == (
445            "+proj=aea +lat_0=23 +lon_0=-96 +lat_1=29.5 "
446            "+lat_2=45.5 +x_0=0 +y_0=0 +datum=NAD83 +units=m +no_defs +type=crs"
447        )
448
449
450def test_compound_crs():
451    wkt = """COMPD_CS["unknown",GEOGCS["WGS 84",DATUM["WGS_1984",
452             SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],
453             TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6326"]],
454             PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433],
455             AUTHORITY["EPSG","4326"]],VERT_CS["unknown",
456             VERT_DATUM["unknown",2005],UNIT["metre",1.0,
457             AUTHORITY["EPSG","9001"]],AXIS["Up",UP]]]"""
458    assert CRS(wkt).to_wkt("WKT1_GDAL").startswith('COMPD_CS["unknown",GEOGCS["WGS 84"')
459
460
461def test_ellipsoid():
462    crs1 = CRS.from_epsg(4326)
463    assert f"{crs1.ellipsoid.inverse_flattening:.3f}" == "298.257"
464    assert f"{crs1.ellipsoid.semi_major_metre:.3f}" == "6378137.000"
465    assert f"{crs1.ellipsoid.semi_minor_metre:.3f}" == "6356752.314"
466
467
468def test_ellipsoid__semi_minor_not_computed():
469    cc = CRS("+proj=geos +lon_0=-89.5 +a=6378137.0 +b=6356752.31 h=12345")
470    assert cc.datum.ellipsoid.semi_minor_metre == 6356752.31
471    assert cc.datum.ellipsoid.semi_major_metre == 6378137.0
472    assert not cc.datum.ellipsoid.is_semi_minor_computed
473
474
475def test_area_of_use():
476    crs1 = CRS.from_epsg(4326)
477    assert crs1.area_of_use.bounds == (-180.0, -90.0, 180.0, 90.0)
478    assert crs1.area_of_use.name == "World."
479
480
481def test_from_user_input_custom_crs_class():
482    assert CRS.from_user_input(CustomCRS()) == CRS.from_epsg(4326)
483
484
485def test_non_crs_error():
486    with pytest.raises(CRSError, match="Input is not a CRS"):
487        CRS(
488            "+proj=pipeline +ellps=GRS80 +step +proj=merc "
489            "+step +proj=axisswap +order=2,1"
490        )
491
492
493def test_sub_crs():
494    crs = CRS.from_epsg(5972)
495    sub_crs_list = crs.sub_crs_list
496    assert len(sub_crs_list) == 2
497    assert sub_crs_list[0] == CRS.from_epsg(25832)
498    assert sub_crs_list[1] == CRS.from_epsg(5941)
499    assert crs.is_projected
500    assert crs.is_vertical
501    assert not crs.is_geographic
502
503
504def test_sub_crs__none():
505    assert CRS.from_epsg(4326).sub_crs_list == []
506
507
508def test_coordinate_system():
509    crs = CRS.from_epsg(26915)
510    assert repr(crs.coordinate_system).startswith("CS[Cartesian")
511    assert crs.coordinate_system.name == "cartesian"
512    assert crs.coordinate_system.name == str(crs.coordinate_system)
513    assert crs.coordinate_system.axis_list == crs.axis_info
514    assert len(crs.coordinate_system.axis_list) == 2
515
516
517def test_coordinate_system_geog():
518    crs = CRS.from_epsg(4326)
519    assert repr(crs.coordinate_system).startswith("CS[ellipsoidal")
520    assert crs.coordinate_system.name == "ellipsoidal"
521    assert crs.coordinate_system.name == str(crs.coordinate_system)
522    assert crs.coordinate_system.axis_list == crs.axis_info
523    assert repr(crs.coordinate_system.axis_list) == (
524        "[Axis(name=Geodetic latitude, abbrev=Lat, direction=north, "
525        "unit_auth_code=EPSG, unit_code=9122, unit_name=degree), "
526        "Axis(name=Geodetic longitude, abbrev=Lon, direction=east, "
527        "unit_auth_code=EPSG, unit_code=9122, unit_name=degree)]"
528    )
529
530
531def test_coordinate_operation():
532    crs = CRS.from_epsg(26915)
533    assert repr(crs.coordinate_operation) == (
534        "<Coordinate Operation: Conversion>\n"
535        "Name: UTM zone 15N\n"
536        "Method: Transverse Mercator\n"
537        "Area of Use:\n"
538        "- name: Between 96°W and 90°W, northern hemisphere between equator and 84°N, "
539        "onshore and offshore.\n"
540        "- bounds: (-96.0, 0.0, -90.0, 84.0)"
541    )
542    assert crs.coordinate_operation.method_name == "Transverse Mercator"
543    assert crs.coordinate_operation.name == str(crs.coordinate_operation)
544    assert crs.coordinate_operation.method_auth_name == "EPSG"
545    assert crs.coordinate_operation.method_code == "9807"
546    assert crs.coordinate_operation.is_instantiable == 1
547    assert crs.coordinate_operation.has_ballpark_transformation == 0
548    assert crs.coordinate_operation.accuracy == -1.0
549    assert repr(crs.coordinate_operation.params) == (
550        "[Param(name=Latitude of natural origin, auth_name=EPSG, code=8801, "
551        "value=0.0, unit_name=degree, unit_auth_name=EPSG, "
552        "unit_code=9102, unit_category=angular), "
553        "Param(name=Longitude of natural origin, auth_name=EPSG, code=8802, "
554        "value=-93.0, unit_name=degree, unit_auth_name=EPSG, "
555        "unit_code=9102, unit_category=angular), "
556        "Param(name=Scale factor at natural origin, auth_name=EPSG, code=8805, "
557        "value=0.9996, unit_name=unity, unit_auth_name=EPSG, "
558        "unit_code=9201, unit_category=scale), "
559        "Param(name=False easting, auth_name=EPSG, code=8806, value=500000.0, "
560        "unit_name=metre, unit_auth_name=EPSG, unit_code=9001, unit_category=linear), "
561        "Param(name=False northing, auth_name=EPSG, code=8807, value=0.0, "
562        "unit_name=metre, unit_auth_name=EPSG, unit_code=9001, unit_category=linear)]"
563    )
564    assert crs.coordinate_operation.grids == []
565
566
567def test_coordinate_operation_grids():
568    cc = CoordinateOperation.from_epsg(1312)
569    if not cc.grids[0].full_name:
570        assert (
571            repr(cc.grids)
572            == "[Grid(short_name=NTv1_0.gsb, full_name=, package_name=, url=, "
573            "direct_download=False, open_license=False, available=False)]"
574        )
575    else:
576        assert (
577            repr(cc.grids)
578            == "[Grid(short_name=NTv1_0.gsb, full_name=NTv1_0.gsb, package_name=, "
579            "url=, direct_download=False, open_license=False, available=False)]"
580        )
581
582
583@pytest.mark.grid
584def test_coordinate_operation_grids__alternative_grid_name():
585    cc = CoordinateOperation.from_epsg(1312, True)
586    assert len(cc.grids) == 1
587    grid = cc.grids[0]
588    assert grid.direct_download is True
589    assert grid.open_license is True
590    assert grid.short_name == "ca_nrc_ntv1_can.tif"
591    if grids_available(grid.short_name):
592        assert grid.available is True
593        assert grid.full_name.endswith(grid.short_name)
594    else:
595        assert grid.available is False
596        assert grid.full_name == ""
597    assert grid.package_name == ""
598    assert grid.url == "https://cdn.proj.org/ca_nrc_ntv1_can.tif"
599
600
601def test_coordinate_operation__missing():
602    crs = CRS.from_epsg(4326)
603    assert crs.coordinate_operation is None
604
605
606def test_coordinate_operation__from_epsg():
607    cc = CoordinateOperation.from_epsg(16031)
608    assert cc.method_auth_name == "EPSG"
609    assert cc.method_code == "9807"
610
611
612def test_coordinate_operation__from_authority():
613    cc = CoordinateOperation.from_authority("EPSG", 16031)
614    assert cc.method_auth_name == "EPSG"
615    assert cc.method_code == "9807"
616
617
618@pytest.mark.parametrize(
619    "user_input",
620    [
621        1671,
622        ("EPSG", 1671),
623        "urn:ogc:def:coordinateOperation:EPSG::1671",
624        CoordinateOperation.from_epsg(1671),
625        CoordinateOperation.from_epsg(1671).to_json_dict(),
626        "RGF93 to WGS 84 (1)",
627    ],
628)
629def test_coordinate_operation__from_user_input(user_input):
630    assert CoordinateOperation.from_user_input(
631        user_input
632    ) == CoordinateOperation.from_epsg(1671)
633
634
635def test_coordinate_operation__from_user_input__invalid():
636    with pytest.raises(CRSError, match="Invalid coordinate operation"):
637        CoordinateOperation.from_user_input({})
638
639
640def test_coordinate_operation__from_epsg__empty():
641    with pytest.raises(CRSError, match="Invalid authority"):
642        CoordinateOperation.from_epsg(1)
643
644
645def test_coordinate_operation__from_authority__empty():
646    with pytest.raises(CRSError, match="Invalid authority"):
647        CoordinateOperation.from_authority("BOB", 4326)
648
649
650def test_datum__from_epsg():
651    if PROJ_GTE_8:
652        datum_wkt = (
653            'ENSEMBLE["World Geodetic System 1984 ensemble",'
654            'MEMBER["World Geodetic System 1984 (Transit)",'
655            'ID["EPSG",1166]],MEMBER["World Geodetic System 1984 (G730)",'
656            'ID["EPSG",1152]],MEMBER["World Geodetic System 1984 (G873)",'
657            'ID["EPSG",1153]],MEMBER["World Geodetic System 1984 (G1150)",'
658            'ID["EPSG",1154]],MEMBER["World Geodetic System 1984 (G1674)",'
659            'ID["EPSG",1155]],MEMBER["World Geodetic System 1984 (G1762)",'
660            'ID["EPSG",1156]],ELLIPSOID["WGS 84",6378137,298.257223563,'
661            'LENGTHUNIT["metre",1],ID["EPSG",7030]],'
662            'ENSEMBLEACCURACY[2.0],ID["EPSG",6326]]'
663        )
664    else:
665        datum_wkt = (
666            'DATUM["World Geodetic System 1984",'
667            'ELLIPSOID["WGS 84",6378137,298.257223563,'
668            'LENGTHUNIT["metre",1]],ID["EPSG",6326]]'
669        )
670    assert Datum.from_epsg("6326").to_wkt() == datum_wkt
671
672
673def test_datum__from_authority():
674    dt = Datum.from_authority("EPSG", 6326)
675    assert dt.name == get_wgs84_datum_name()
676
677
678def test_datum__from_epsg__invalid():
679    with pytest.raises(CRSError, match="Invalid authority"):
680        Datum.from_epsg(1)
681
682
683def test_datum__from_authority__invalid():
684    with pytest.raises(CRSError, match="Invalid authority"):
685        Datum.from_authority("BOB", 1)
686
687
688@pytest.mark.parametrize(
689    "user_input",
690    [
691        6326,
692        ("EPSG", 6326),
693        "urn:ogc:def:ensemble:EPSG::6326"
694        if PROJ_GTE_8
695        else "urn:ogc:def:datum:EPSG::6326",
696        Datum.from_epsg(6326),
697        Datum.from_epsg(6326).to_json_dict(),
698        "World Geodetic System 1984",
699    ],
700)
701def test_datum__from_user_input(user_input):
702    assert Datum.from_user_input(user_input) == Datum.from_epsg(6326)
703
704
705def test_datum__from_user_input__invalid():
706    with pytest.raises(CRSError, match="Invalid datum"):
707        Datum.from_user_input({})
708
709
710def test_prime_meridian__from_epsg():
711    assert PrimeMeridian.from_epsg(8903).to_wkt() == (
712        'PRIMEM["Paris",2.5969213,ANGLEUNIT["grad",0.0157079632679489],ID["EPSG",8903]]'
713    )
714
715
716def test_prime_meridian__from_authority():
717    assert PrimeMeridian.from_authority("EPSG", 8903).name == "Paris"
718
719
720def test_prime_meridian__from_epsg__invalid():
721    with pytest.raises(CRSError, match="Invalid authority"):
722        PrimeMeridian.from_epsg(1)
723
724
725def test_prime_meridian__from_authority__invalid():
726    with pytest.raises(CRSError, match="Invalid authority"):
727        PrimeMeridian.from_authority("Bob", 1)
728
729
730@pytest.mark.parametrize(
731    "user_input",
732    [
733        8901,
734        ("EPSG", 8901),
735        "urn:ogc:def:meridian:EPSG::8901",
736        PrimeMeridian.from_epsg(8901),
737        PrimeMeridian.from_epsg(8901).to_json_dict(),
738        "Greenwich",
739    ],
740)
741def test_prime_meridian__from_user_input(user_input):
742    assert PrimeMeridian.from_user_input(user_input) == PrimeMeridian.from_epsg(8901)
743
744
745def test_prime_meridian__from_user_input__invalid():
746    with pytest.raises(CRSError, match="Invalid prime meridian"):
747        PrimeMeridian.from_user_input({})
748
749
750def test_ellipsoid__from_epsg():
751    assert Ellipsoid.from_epsg(7030).to_wkt() == (
752        'ELLIPSOID["WGS 84",6378137,298.257223563,'
753        'LENGTHUNIT["metre",1],ID["EPSG",7030]]'
754    )
755
756
757def test_ellipsoid__from_authority():
758    assert Ellipsoid.from_authority("EPSG", 7030).name == "WGS 84"
759
760
761def test_ellipsoid__from_epsg__invalid():
762    with pytest.raises(CRSError, match="Invalid authority"):
763        Ellipsoid.from_epsg(1)
764
765
766def test_ellipsoid__from_authority__invalid():
767    with pytest.raises(CRSError, match="Invalid authority"):
768        Ellipsoid.from_authority("BOB", 1)
769
770
771@pytest.mark.parametrize(
772    "user_input",
773    [
774        7001,
775        ("EPSG", 7001),
776        "urn:ogc:def:ellipsoid:EPSG::7001",
777        Ellipsoid.from_epsg(7001),
778        Ellipsoid.from_epsg(7001).to_json_dict(),
779        "Airy 1830",
780    ],
781)
782def test_ellipsoid__from_user_input(user_input):
783    assert Ellipsoid.from_user_input(user_input) == Ellipsoid.from_epsg(7001)
784
785
786def test_ellipsoid__from_user_input__invalid():
787    with pytest.raises(CRSError, match="Invalid ellipsoid"):
788        Ellipsoid.from_user_input({})
789
790
791CS_JSON_DICT = {
792    "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json",
793    "type": "CoordinateSystem",
794    "subtype": "Cartesian",
795    "axis": [
796        {"name": "Easting", "abbreviation": "E", "direction": "east", "unit": "metre"},
797        {
798            "name": "Northing",
799            "abbreviation": "N",
800            "direction": "north",
801            "unit": "metre",
802        },
803    ],
804}
805
806
807@pytest.mark.parametrize(
808    "user_input",
809    [
810        CS_JSON_DICT,
811        json.dumps(CS_JSON_DICT),
812        CoordinateSystem.from_json_dict(CS_JSON_DICT),
813    ],
814)
815def test_coordinate_system__from_user_input(user_input):
816    assert CoordinateSystem.from_user_input(
817        user_input
818    ) == CoordinateSystem.from_json_dict(CS_JSON_DICT)
819
820
821@pytest.mark.parametrize(
822    "user_input",
823    [
824        7001,
825        ("EPSG", 7001),
826        "urn:ogc:def:ellipsoid:EPSG::7001",
827        Ellipsoid.from_epsg(7001),
828        Ellipsoid.from_epsg(7001).to_json_dict(),
829    ],
830)
831def test_coordinate_system__from_user_input__invalid(user_input):
832    with pytest.raises(CRSError, match="Invalid"):
833        CoordinateSystem.from_user_input(user_input)
834
835
836def test_bound_crs_is_geographic():
837    assert CRS(
838        "proj=longlat datum=WGS84 no_defs ellps=WGS84 towgs84=0,0,0"
839    ).is_geographic
840
841
842def test_coordinate_operation_towgs84_three():
843    crs = CRS("+proj=latlong +ellps=GRS80 +towgs84=-199.87,74.79,246.62")
844    assert crs.coordinate_operation.towgs84 == [-199.87, 74.79, 246.62]
845
846
847def test_coordinate_operation_towgs84_seven():
848    crs = CRS(
849        "+proj=tmerc +lat_0=0 +lon_0=15 +k=0.9996 +x_0=2520000 +y_0=0 "
850        "+ellps=intl +towgs84=-122.74,-34.27,-22.83,-1.884,-3.400,-3.030,-15.62"
851    )
852    assert crs.coordinate_operation.towgs84 == [
853        -122.74,
854        -34.27,
855        -22.83,
856        -1.884,
857        -3.4,
858        -3.03,
859        -15.62,
860    ]
861
862
863def test_axis_info_bound():
864    crs = CRS(
865        "+proj=tmerc +lat_0=0 +lon_0=15 +k=0.9996 +x_0=2520000 +y_0=0 "
866        "+ellps=intl +towgs84=-122.74,-34.27,-22.83,-1.884,-3.400,-3.030,-15.62"
867    )
868    assert [axis.direction for axis in crs.axis_info] == ["east", "north"]
869
870
871def test_coordinate_operation_towgs84_missing():
872    crs = CRS("epsg:3004")
873    assert crs.coordinate_operation.towgs84 == []
874
875
876@pytest.mark.parametrize(
877    "wkt_version_str, wkt_version_enum",
878    [
879        ("WKT1_GDAL", WktVersion.WKT1_GDAL),
880        ("WKT2_2018", WktVersion.WKT2_2018),
881        ("WKT2_2018_SIMPLIFIED", WktVersion.WKT2_2018_SIMPLIFIED),
882        ("WKT2_2019", WktVersion.WKT2_2019),
883        ("WKT2_2019_SIMPLIFIED", WktVersion.WKT2_2019_SIMPLIFIED),
884        ("WKT2_2015", WktVersion.WKT2_2015),
885        ("WKT2_2015_SIMPLIFIED", WktVersion.WKT2_2015_SIMPLIFIED),
886    ],
887)
888def test_to_wkt_enum(wkt_version_str, wkt_version_enum):
889    crs = CRS.from_epsg(4326)
890    assert crs.to_wkt(wkt_version_str) == crs.to_wkt(wkt_version_enum)
891
892
893def test_to_wkt_enum__invalid():
894    crs = CRS.from_epsg(4326)
895    with pytest.raises(ValueError, match="Invalid value"):
896        crs.to_wkt("WKT_INVALID")
897
898
899def test_to_proj4_enum():
900    crs = CRS.from_epsg(4326)
901    with pytest.warns(UserWarning):
902        assert crs.to_proj4(4) == crs.to_proj4(ProjVersion.PROJ_4)
903        assert crs.to_proj4(5) == crs.to_proj4(ProjVersion.PROJ_5)
904
905
906def test_datum_equals():
907    datum = Datum.from_epsg(6326)
908    assert datum == 6326
909    assert not datum != 6326
910    assert datum != "invalid"
911
912
913@pytest.mark.parametrize(
914    "input_str",
915    [
916        "urn:ogc:def:ensemble:EPSG::6326"
917        if PROJ_GTE_8
918        else "urn:ogc:def:datum:EPSG::6326",
919        "World Geodetic System 1984",
920    ],
921)
922def test_datum__from_string(input_str):
923    dd = Datum.from_string(input_str)
924    if PROJ_GTE_8:
925        assert dd.name == "World Geodetic System 1984 ensemble"
926        assert dd.type_name == "Datum Ensemble"
927    else:
928        assert dd.name == "World Geodetic System 1984"
929        assert dd.type_name == "Geodetic Reference Frame"
930
931
932@pytest.mark.parametrize(
933    "input_str, type_name",
934    [
935        ('ENGINEERINGDATUM["Engineering datum"]', "Engineering Datum"),
936        ('PDATUM["Mean Sea Level",ANCHOR["1013.25 hPa at 15°C"]]', "Parametric Datum"),
937        (
938            'TDATUM["Gregorian calendar",CALENDAR["proleptic Gregorian"],'
939            "TIMEORIGIN[0000-01-01]]",
940            "Temporal Datum",
941        ),
942    ],
943)
944def test_datum__from_string__type_name(input_str, type_name):
945    dd = Datum.from_string(input_str)
946    assert dd.type_name == type_name
947
948
949@pytest.mark.parametrize(
950    "input_name", ["World Geodetic System 1984", "WGS84", "WGS 84"]
951)
952def test_datum__from_name(input_name):
953    dd = Datum.from_name(input_name)
954    assert dd.name == get_wgs84_datum_name()
955
956
957@pytest.mark.parametrize("auth_name", [None, "ESRI"])
958def test_datum_from_name__auth_type(auth_name):
959    dd = Datum.from_name(
960        "WGS_1984_Geoid",
961        auth_name=auth_name,
962        datum_type=DatumType.VERTICAL_REFERENCE_FRAME,
963    )
964    assert dd.name == "WGS_1984_Geoid"
965    assert dd.type_name == "Vertical Reference Frame"
966
967
968def test_datum_from_name__any_type():
969    dd = Datum.from_name("WGS_1984_Geoid")
970    assert dd.name == "WGS_1984_Geoid"
971    assert dd.type_name == "Vertical Reference Frame"
972
973
974@pytest.mark.parametrize(
975    "invalid_str", ["3-598y5-98y", "urn:ogc:def:ellipsoid:EPSG::7001"]
976)
977def test_datum__from_name__invalid(invalid_str):
978    with pytest.raises(CRSError, match="Invalid datum name:"):
979        Datum.from_name(invalid_str)
980
981
982def test_datum__from_name__invalid_type():
983    with pytest.raises(CRSError, match="Invalid datum name: WGS84"):
984        Datum.from_name("WGS84", datum_type="VERTICAL_REFERENCE_FRAME")
985
986
987@pytest.mark.parametrize(
988    "invalid_str", ["3-598y5-98y", "urn:ogc:def:ellipsoid:EPSG::7001"]
989)
990def test_datum__from_string__invalid(invalid_str):
991    with pytest.raises(CRSError, match="Invalid datum string"):
992        Datum.from_string(invalid_str)
993
994
995def test_ellipsoid_equals():
996    ellipsoid = Ellipsoid.from_epsg(7001)
997    assert ellipsoid == 7001
998    assert not ellipsoid != 7001
999    assert ellipsoid != "invalid"
1000
1001
1002@pytest.mark.parametrize("input_str", ["urn:ogc:def:ellipsoid:EPSG::7001", "Airy 1830"])
1003def test_ellipsoid__from_string(input_str):
1004    ee = Ellipsoid.from_string(input_str)
1005    assert ee.name == "Airy 1830"
1006
1007
1008@pytest.mark.parametrize(
1009    "input_str,long_name",
1010    [
1011        ("Airy 1830", "Airy 1830"),
1012        ("intl", HAYFORD_ELLIPSOID_NAME),
1013        (HAYFORD_ELLIPSOID_NAME, HAYFORD_ELLIPSOID_NAME),
1014    ],
1015)
1016def test_ellipsoid__from_name(input_str, long_name):
1017    ee = Ellipsoid.from_name(input_str)
1018    assert ee.name == long_name
1019
1020
1021@pytest.mark.parametrize("invalid_str", ["3-598y5-98y", "urn:ogc:def:datum:EPSG::6326"])
1022def test_ellipsoid__from_name__invalid(invalid_str):
1023    with pytest.raises(CRSError, match="Invalid ellipsoid name"):
1024        Ellipsoid.from_name(invalid_str)
1025
1026
1027def test_ellipsoid__from_name__invalid__auth():
1028    with pytest.raises(CRSError, match="Invalid ellipsoid name"):
1029        Ellipsoid.from_name("intl", auth_name="ESRI")
1030
1031
1032@pytest.mark.parametrize("invalid_str", ["3-598y5-98y", "urn:ogc:def:datum:EPSG::6326"])
1033def test_ellipsoid__from_string__invalid(invalid_str):
1034    with pytest.raises(CRSError, match="Invalid ellipsoid string"):
1035        Ellipsoid.from_string(invalid_str)
1036
1037
1038def test_prime_meridian_equals():
1039    pm = PrimeMeridian.from_epsg(8901)
1040    assert pm == 8901
1041    assert not pm != 8901
1042    assert pm != "invalid"
1043
1044
1045@pytest.mark.parametrize("input_str", ["urn:ogc:def:meridian:EPSG::8901", "Greenwich"])
1046def test_prime_meridian__from_string(input_str):
1047    pm = PrimeMeridian.from_string(input_str)
1048    assert pm.name == "Greenwich"
1049
1050
1051@pytest.mark.parametrize("invalid_str", ["3-598y5-98y", "urn:ogc:def:datum:EPSG::6326"])
1052def test_prime_meridian__from_string__invalid(invalid_str):
1053    with pytest.raises(CRSError, match="Invalid prime meridian string"):
1054        PrimeMeridian.from_string(invalid_str)
1055
1056
1057def test_prime_meridian__from_name():
1058    pm = PrimeMeridian.from_name("Greenwich")
1059    assert pm.name == "Greenwich"
1060
1061
1062@pytest.mark.parametrize("invalid_str", ["3-598y5-98y", "urn:ogc:def:datum:EPSG::6326"])
1063def test_prime_meridian__from_name__invalid(invalid_str):
1064    with pytest.raises(CRSError, match="Invalid prime meridian name"):
1065        PrimeMeridian.from_name(invalid_str)
1066
1067
1068def test_coordinate_operation_equals():
1069    co = CoordinateOperation.from_epsg(1671)
1070    assert co == 1671
1071    assert not co != 1671
1072    assert co != "invalid"
1073
1074
1075@pytest.mark.parametrize(
1076    "input_str", ["urn:ogc:def:coordinateOperation:EPSG::1671", "RGF93 to WGS 84 (1)"]
1077)
1078def test_coordinate_operation__from_string(input_str):
1079    co = CoordinateOperation.from_string(input_str)
1080    assert co.name == "RGF93 to WGS 84 (1)"
1081
1082
1083def test_coordinate_operation__from_name():
1084    co = CoordinateOperation.from_name("UTM zone 12N")
1085    assert co.name == "UTM zone 12N"
1086
1087
1088def test_coordinate_operation__from_name_auth_type():
1089    co = CoordinateOperation.from_name(
1090        "ITRF_2000_To_WGS_1984",
1091        auth_name="ESRI",
1092        coordinate_operation_type=CoordinateOperationType.TRANSFORMATION,
1093    )
1094    assert co.name == "ITRF_2000_To_WGS_1984"
1095
1096
1097@pytest.mark.parametrize("invalid_str", ["3-598y5-98y", "urn:ogc:def:datum:EPSG::6326"])
1098def test_coordinate_operation__from_name__invalid(invalid_str):
1099    with pytest.raises(CRSError, match="Invalid coordinate operation name"):
1100        CoordinateOperation.from_name(invalid_str)
1101
1102
1103@pytest.mark.parametrize("invalid_str", ["3-598y5-98y", "urn:ogc:def:datum:EPSG::6326"])
1104def test_coordinate_operation__from_string__invalid(invalid_str):
1105    with pytest.raises(CRSError, match="Invalid coordinate operation string"):
1106        CoordinateOperation.from_string(invalid_str)
1107
1108
1109_COORDINATE_SYSTEM_STR = (
1110    '{"$schema":"https://proj.org/schemas/v0.2/projjson.schema.json",'
1111    '"type":"CoordinateSystem","subtype":"ellipsoidal",'
1112    '"axis":[{"name":"Geodetic latitude","abbreviation":"Lat",'
1113    '"direction":"north","unit":"degree"},'
1114    '{"name":"Geodetic longitude","abbreviation":"Lon",'
1115    '"direction":"east","unit":"degree"}],'
1116    '"id":{"authority":"EPSG","code":6422}}'
1117)
1118
1119
1120def test_coordinate_system__equals():
1121    cs = CoordinateSystem.from_string(_COORDINATE_SYSTEM_STR)
1122    assert cs == _COORDINATE_SYSTEM_STR
1123    assert not cs != _COORDINATE_SYSTEM_STR
1124    assert cs != "invalid"
1125
1126
1127def test_coordinate_system__from_string():
1128    cs = CoordinateSystem.from_string(_COORDINATE_SYSTEM_STR)
1129    assert cs.name == "ellipsoidal"
1130
1131
1132@pytest.mark.parametrize(
1133    "invalid_cs_string", ["3-598y5-98y", "urn:ogc:def:datum:EPSG::6326"]
1134)
1135def test_coordinate_system__from_string__invalid(invalid_cs_string):
1136    with pytest.raises(CRSError, match="Invalid coordinate system string"):
1137        CoordinateSystem.from_string(invalid_cs_string)
1138
1139
1140def test_to_proj4_enum__invalid():
1141    crs = CRS.from_epsg(4326)
1142    with pytest.raises(ValueError, match="Invalid value"), pytest.warns(UserWarning):
1143        crs.to_proj4(1)
1144
1145
1146def test_geodetic_crs():
1147    cc = CRS("epsg:3004")
1148    assert cc.geodetic_crs.to_epsg() == 4265
1149
1150
1151def test_itrf_init():
1152    crs = CRS("ITRF2000")
1153    assert crs.name == "ITRF2000"
1154
1155
1156def test_compound_crs_init():
1157    crs = CRS("EPSG:2393+5717")
1158    assert crs.name == "KKJ / Finland Uniform Coordinate System + N60 height"
1159
1160
1161def test_compound_crs_urn_init():
1162    crs = CRS("urn:ogc:def:crs,crs:EPSG::2393,crs:EPSG::5717")
1163    assert crs.name == "KKJ / Finland Uniform Coordinate System + N60 height"
1164
1165
1166def test_from_authority__ignf():
1167    cc = CRS.from_authority("IGNF", "ETRS89UTM28")
1168    assert cc.to_authority() == ("IGNF", "ETRS89UTM28")
1169    assert cc.to_authority("EPSG") == ("EPSG", "25828")
1170    assert cc.to_epsg() == 25828
1171
1172
1173def test_ignf_authority_repr():
1174    assert repr(CRS.from_authority("IGNF", "ETRS89UTM28")).startswith(
1175        "<Projected CRS: IGNF:ETRS89UTM28>"
1176    )
1177
1178
1179def test_crs_hash():
1180    """hashes of equivalent CRS are equal"""
1181    assert hash(CRS.from_epsg(3857)) == hash(CRS.from_epsg(3857))
1182
1183
1184def test_crs_hash_unequal():
1185    """hashes of non-equivalent CRS are not equal"""
1186    assert hash(CRS.from_epsg(3857)) != hash(CRS.from_epsg(4326))
1187
1188
1189def test_crs_init_user_input():
1190    assert CRS(("IGNF", "ETRS89UTM28")).to_authority() == ("IGNF", "ETRS89UTM28")
1191    assert CRS(4326).to_epsg() == 4326
1192
1193    proj4_dict = {"proj": "longlat", "datum": "WGS84", "no_defs": None, "type": "crs"}
1194    with pytest.warns(UserWarning):
1195        assert CRS({"proj": "lonlat", "datum": "WGS84"}).to_dict() == proj4_dict
1196        assert CRS(proj="lonlat", datum="WGS84").to_dict() == proj4_dict
1197        assert (
1198            CRS('{"proj": "longlat", "ellps": "WGS84", "datum": "WGS84"}').to_dict()
1199            == proj4_dict
1200        )
1201    assert CRS(CRS(4326)).is_exact_same(CRS(CustomCRS()))
1202
1203
1204def test_crs_is_exact_same__non_crs_input():
1205    assert CRS(4326).is_exact_same("epsg:4326")
1206    with pytest.warns(FutureWarning):
1207        assert not CRS(4326).is_exact_same("+init=epsg:4326")
1208
1209
1210def test_to_string__no_auth():
1211    proj = CRS("+proj=latlong +ellps=GRS80 +towgs84=-199.87,74.79,246.62")
1212    assert (
1213        proj.to_string()
1214        == "+proj=latlong +ellps=GRS80 +towgs84=-199.87,74.79,246.62 +type=crs"
1215    )
1216
1217
1218def test_to_string__auth():
1219    assert CRS(("IGNF", "ETRS89UTM28")).to_string() == "IGNF:ETRS89UTM28"
1220
1221
1222def test_srs__no_plus():
1223    assert (
1224        CRS("proj=longlat datum=WGS84 no_defs").srs
1225        == "proj=longlat datum=WGS84 no_defs type=crs"
1226    )
1227
1228
1229def test_equals_different_type():
1230    assert CRS("epsg:4326") != ""
1231    assert not CRS("epsg:4326") == ""
1232
1233    assert CRS("epsg:4326") != 27700
1234    assert not CRS("epsg:4326") == 27700
1235
1236    assert not CRS("epsg:4326") != 4326
1237    assert CRS("epsg:4326") == 4326
1238
1239
1240def test_is_exact_same_different_type():
1241    assert not CRS("epsg:4326").is_exact_same(None)
1242
1243
1244def test_compare_crs_non_crs():
1245    assert CRS.from_epsg(4326) != 4.2
1246    assert CRS.from_epsg(4326) == 4326
1247    with pytest.warns(FutureWarning):
1248        assert CRS.from_dict({"init": "epsg:4326"}) == {"init": "epsg:4326"}
1249        assert CRS.from_dict({"init": "epsg:4326"}) != "epsg:4326"
1250    assert CRS("epsg:4326") == CustomCRS()
1251
1252
1253def test_is_geocentric__bound():
1254    with pytest.warns(FutureWarning):
1255        ccs = CRS("+init=epsg:4328 +towgs84=0,0,0")
1256    assert ccs.is_geocentric
1257
1258
1259def test_is_geocentric():
1260    ccs = CRS.from_epsg(4328)
1261    assert ccs.is_geocentric
1262
1263
1264def test_is_vertical():
1265    cc = CRS.from_epsg(5717)
1266    assert cc.is_vertical
1267
1268
1269def test_is_engineering():
1270    eng_wkt = (
1271        'ENGCRS["A construction site CRS",\n'
1272        'EDATUM["P1",ANCHOR["Peg in south corner"]],\n'
1273        'CS[Cartesian,2],\nAXIS["site east",southWest,ORDER[1]],\n'
1274        'AXIS["site north",southEast,ORDER[2]],\n'
1275        'LENGTHUNIT["metre",1.0],\n'
1276        'TIMEEXTENT["date/time t1","date/time t2"]]'
1277    )
1278    assert CRS(eng_wkt).is_engineering
1279
1280
1281def test_source_crs__bound():
1282    with pytest.warns(FutureWarning):
1283        assert CRS("+init=epsg:4328 +towgs84=0,0,0").source_crs.name == "unknown"
1284
1285
1286def test_source_crs__missing():
1287    assert CRS("epsg:4326").source_crs is None
1288
1289
1290def test_target_crs__bound():
1291    with pytest.warns(FutureWarning):
1292        assert CRS("+init=epsg:4328 +towgs84=0,0,0").target_crs.name == "WGS 84"
1293
1294
1295def test_target_crs__missing():
1296    assert CRS("epsg:4326").target_crs is None
1297
1298
1299def test_whitepace_between_equals():
1300    crs = CRS(
1301        "+proj =lcc +lat_1= 30.0 +lat_2= 35.0 +lat_0=30.0 +lon_0=87.0 +x_0=0 +y_0=0"
1302    )
1303    assert crs.srs == (
1304        "+proj=lcc +lat_1=30.0 +lat_2=35.0 +lat_0=30.0 "
1305        "+lon_0=87.0 +x_0=0 +y_0=0 +type=crs"
1306    )
1307
1308
1309def test_to_dict_no_proj4():
1310    crs = CRS(
1311        {
1312            "a": 6371229.0,
1313            "b": 6371229.0,
1314            "lon_0": -10.0,
1315            "o_lat_p": 30.0,
1316            "o_lon_p": 0.0,
1317            "o_proj": "longlat",
1318            "proj": "ob_tran",
1319        }
1320    )
1321    with pytest.warns(UserWarning):
1322        assert crs.to_dict() == {
1323            "R": 6371229,
1324            "lon_0": -10,
1325            "no_defs": None,
1326            "o_lat_p": 30,
1327            "o_lon_p": 0,
1328            "o_proj": "longlat",
1329            "proj": "ob_tran",
1330            "type": "crs",
1331        }
1332
1333
1334def test_to_dict_from_dict():
1335    cc = CRS.from_epsg(4326)
1336    with pytest.warns(UserWarning):
1337        assert CRS.from_dict(cc.to_dict()).name == "unknown"
1338
1339
1340def test_from_dict__invalid():
1341    with pytest.raises(CRSError, match="CRS input is not a dict"):
1342        CRS.from_dict(4326)
1343
1344
1345@pytest.mark.parametrize(
1346    "class_type",
1347    [Datum, Ellipsoid, PrimeMeridian, CoordinateOperation, CoordinateSystem],
1348)
1349def test_incorrectly_initialized(class_type):
1350    with pytest.raises(RuntimeError):
1351        class_type()
1352
1353
1354def test_scope__remarks():
1355    co = CoordinateOperation.from_epsg("8048")
1356    assert "GDA94" in co.scope
1357    assert "Scale difference" in co.remarks
1358
1359
1360def test_crs__scope__remarks__missing():
1361    cc = CRS("+proj=latlon")
1362    assert cc.scope is None
1363    assert cc.remarks is None
1364
1365
1366def test_operations_missing():
1367    cc = CRS(("IGNF", "ETRS89UTM28"))
1368    assert cc.coordinate_operation.operations == ()
1369
1370
1371def test_operations():
1372    transformer = TransformerGroup(28356, 7856).transformers[0]
1373    coord_op = CoordinateOperation.from_string(transformer.to_wkt())
1374    assert coord_op.operations == transformer.operations
1375
1376
1377def test_operations__scope_remarks():
1378    operation = TransformerGroup(28356, 7856).transformers[0].operations[1]
1379    coord_op = CoordinateOperation.from_string(operation.to_wkt())
1380    assert coord_op == operation
1381    assert coord_op.remarks == operation.remarks
1382    assert coord_op.scope == operation.scope
1383
1384
1385def test_crs_equals():
1386    assert CRS(4326).equals("epsg:4326")
1387
1388
1389def test_crs_equals__ignore_axis_order():
1390    with pytest.warns(FutureWarning):
1391        assert CRS("epsg:4326").equals("+init=epsg:4326", ignore_axis_order=True)
1392
1393
1394@pytest.mark.parametrize(
1395    "crs_input",
1396    [
1397        "+proj=utm +zone=15",
1398        26915,
1399        "+proj=utm +zone=15 +towgs84=0,0,0",
1400        "EPSG:26915+5717",
1401    ],
1402)
1403def test_utm_zone(crs_input):
1404    assert CRS(crs_input).utm_zone == "15N"
1405
1406
1407@pytest.mark.parametrize("crs_input", ["+proj=tmerc", "epsg:4326"])
1408def test_utm_zone__none(crs_input):
1409    assert CRS(crs_input).utm_zone is None
1410
1411
1412def test_numpy_bool_kwarg_false():
1413    # Issue 564
1414    south = numpy.array(50) < 0
1415    crs = CRS(
1416        proj="utm", zone=32, ellipsis="WGS84", datum="WGS84", units="m", south=south
1417    )
1418    assert "south" not in crs.srs
1419
1420
1421def test_numpy_bool_kwarg_true():
1422    # Issue 564
1423    south = numpy.array(50) > 0
1424    crs = CRS(
1425        proj="utm", zone=32, ellipsis="WGS84", datum="WGS84", units="m", south=south
1426    )
1427    assert "+south " in crs.srs
1428
1429
1430@pytest.mark.skipif(
1431    pyproj._datadir._USE_GLOBAL_CONTEXT, reason="Global Context not Threadsafe."
1432)
1433def test_crs_multithread():
1434    # https://github.com/pyproj4/pyproj/issues/782
1435    crs = CRS(4326)
1436
1437    def to_wkt(num):
1438        return crs.to_wkt()
1439
1440    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
1441        for result in executor.map(to_wkt, range(10)):
1442            pass
1443
1444
1445def test_crs_multiprocess():
1446    # https://github.com/pyproj4/pyproj/issues/933
1447    with concurrent.futures.ProcessPoolExecutor(max_workers=2) as executor:
1448        for result in executor.map(CRS, [4326 for _ in range(10)]):
1449            pass
1450
1451
1452def test_coordinate_operation__to_proj4():
1453    operation = CoordinateOperation.from_string(
1454        "+proj=pipeline +step +proj=axisswap +order=2,1 +step "
1455        "+proj=unitconvert +xy_in=deg +xy_out=rad +step "
1456        "+proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84"
1457    )
1458    proj_string = operation.to_proj4()
1459    assert "+proj=pipeline" in proj_string
1460    assert "\n" not in proj_string
1461
1462
1463def test_coordinate_operation__to_proj4__pretty():
1464    operation = CoordinateOperation.from_string(
1465        "+proj=pipeline +step +proj=axisswap +order=2,1 +step "
1466        "+proj=unitconvert +xy_in=deg +xy_out=rad +step "
1467        "+proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84"
1468    )
1469    proj_string = operation.to_proj4(pretty=True)
1470    assert "+proj=pipeline" in proj_string
1471    assert "\n" in proj_string
1472
1473
1474@pytest.mark.parametrize(
1475    "crs_input",
1476    [
1477        "EPSG:4326",
1478        "EPSG:2056",
1479    ],
1480)
1481def test_to_3d(crs_input):
1482    crs = CRS(crs_input)
1483    assert len(crs.axis_info) == 2
1484    crs_3d = crs.to_3d()
1485    assert len(crs_3d.axis_info) == 3
1486    vert_axis = crs_3d.axis_info[-1]
1487    assert vert_axis.name == "Ellipsoidal height"
1488    assert vert_axis.unit_name == "metre"
1489    assert vert_axis.direction == "up"
1490    assert crs_3d.to_3d() == crs_3d
1491    assert crs_3d.name == crs.name
1492
1493
1494def test_to_3d__name():
1495    crs_3d = CRS("EPSG:2056").to_3d(name="TEST")
1496    assert crs_3d.name == "TEST"
1497
1498
1499def test_crs__pickle(tmp_path):
1500    assert_can_pickle(CRS("epsg:4326"), tmp_path)
1501
1502
1503def test_is_derived():
1504    assert CRS(
1505        "+proj=ob_tran +o_proj=longlat +o_lat_p=0 +o_lon_p=0 +lon_0=0"
1506    ).is_derived
1507    assert not CRS("+proj=latlon").is_derived
1508
1509
1510def test_inheritance__from_methods():
1511    class ChildCRS(CRS):
1512        def new_method(self):
1513            return 1
1514
1515    def assert_inheritance_valid(new_crs):
1516        assert new_crs.new_method() == 1
1517        assert isinstance(new_crs, ChildCRS)
1518        assert isinstance(new_crs.geodetic_crs, ChildCRS)
1519        assert isinstance(new_crs.source_crs, (type(None), ChildCRS))
1520        assert isinstance(new_crs.target_crs, (type(None), ChildCRS))
1521        assert isinstance(new_crs.to_3d(), ChildCRS)
1522        for sub_crs in new_crs.sub_crs_list:
1523            assert isinstance(sub_crs, ChildCRS)
1524
1525    assert_inheritance_valid(ChildCRS.from_epsg(4326))
1526    assert_inheritance_valid(ChildCRS.from_string("EPSG:2056"))
1527    with pytest.warns(FutureWarning):
1528        assert_inheritance_valid(ChildCRS.from_proj4("+init=epsg:4328 +towgs84=0,0,0"))
1529    assert_inheritance_valid(ChildCRS.from_user_input("EPSG:4326+5773"))
1530    assert_inheritance_valid(ChildCRS.from_json(CRS(4326).to_json()))
1531    assert_inheritance_valid(ChildCRS.from_json_dict(CRS(4326).to_json_dict()))
1532    assert_inheritance_valid(ChildCRS.from_wkt(CRS(4326).to_wkt()))
1533
1534
1535def test_list_authority():
1536    assert CRS("+proj=utm +zone=15").list_authority() == [
1537        AuthorityMatchInfo(auth_name="EPSG", code="32615", confidence=70)
1538    ]
1539
1540
1541def test_list_authority__multiple():
1542    auth_list = CRS("+proj=longlat").list_authority()
1543    assert AuthorityMatchInfo(auth_name="OGC", code="CRS84", confidence=70) in auth_list
1544    assert AuthorityMatchInfo(auth_name="EPSG", code="4326", confidence=70) in auth_list
1545