diff --git a/general/g.proj/g.proj.html b/general/g.proj/g.proj.html index 89a44c93622..7591d9fa50e 100644 --- a/general/g.proj/g.proj.html +++ b/general/g.proj/g.proj.html @@ -20,7 +20,7 @@

DESCRIPTION

When compiled with OGR, functionality is increased and allows output of the CRS information in the Well-Known Text (WKT) format popularised -by proprietary GIS. In addition, if one of the parameters georef, +by PROJ and GDAL. In addition, if one of the parameters georef, wkt, proj4 or epsg is specified, rather than being read from the current project, the CRS information is imported from an external source as follows: @@ -110,12 +110,18 @@

NOTES

Output is simply based on the input CRS information. g.proj does not attempt to verify that the co-ordinate system thus -described matches an existing system in use in the world. In particular, -this means there are no EPSG Authority codes in the WKT output. +described matches a pre-defined existing system in use in the world. In +particular, this means there may be no authority names and codes in the +WKT output.

WKT format shows the false eastings and northings in the projected unit (e.g. meters, feet) but in PROJ format it should always be given in meters. +

PROJJSON format is a JSON version of the WKT format, see the PROJJSON +specification + +

The maximum size of input WKT or PROJ CRS descriptions is limited to 8000 bytes. diff --git a/general/g.proj/g.proj.md b/general/g.proj/g.proj.md index c68f9840c8a..65fdef6f115 100644 --- a/general/g.proj/g.proj.md +++ b/general/g.proj/g.proj.md @@ -17,27 +17,31 @@ is limited to: When compiled with OGR, functionality is increased and allows output of the CRS information in the Well-Known Text (WKT) format popularised by -proprietary GIS. In addition, if one of the parameters *georef*, *wkt*, +PROJ and GDAL. In addition, if one of the parameters *georef*, *wkt*, *proj4* or *epsg* is specified, rather than being read from the current project, the CRS information is imported from an external source as follows: -- With **georef**=*filename* g.proj attempts to invoke GDAL and OGR in turn -to read a georeferenced file *filename*. The CRS information will be read -from this file. If the file is not georeferenced or cannot be read, -XY (unprojected) will be used. +georef=*filename* +*g.proj* attempts to invoke GDAL and OGR in turn to read a georeferenced +file *filename*. The CRS information will be read from this file. If the +file is not georeferenced or cannot be read, XY (unprojected) will be +used. -- When using **wkt**=*filename*, the file *filename* should contain a CRS -description in WKT format with or without line-breaks (e.g. a '.prj' file). -If **-** is given for the filename, the WKT description will be read from -stdin rather than a file. +wkt=*filename* or **-** +The file *filename* should contain a CRS description in WKT format with +or without line-breaks (e.g. a '.prj' file). If **-** is given for the +filename, the WKT description will be read from stdin rather than a +file. -- **proj4**=*description* should be a CRS description in [PROJ](https://proj.org/) +proj4=*description* or **-** +*description* should be a CRS description in [PROJ](https://proj.org/) format, enclosed in quotation marks if there are any spaces. If **-** is given for *description*, the PROJ description will be read from stdin rather than as a directly-supplied command-line parameter. -- **epsg**=*number* should correspond to the index number of a valid co-ordinate +epsg=*number* +*number* should correspond to the index number of a valid co-ordinate system in the [EPSG database](https://epsg.org/search/by-name). EPSG code support is based upon a local copy of the GDAL CSV co-ordinate system and datum information files, stored in the directory @@ -95,14 +99,17 @@ co-ordinate system. This can be useful to change the datum information for an existing project. Output is simply based on the input CRS information. g.proj does **not** -attempt to verify that the co-ordinate system thus described matches an -existing system in use in the world. In particular, this means there are -no EPSG Authority codes in the WKT output. +attempt to verify that the co-ordinate system thus described matches a +pre-defined existing system in use in the world. In particular, this +means there may be no authority names and codes in the WKT output. WKT format shows the false eastings and northings in the projected unit (e.g. meters, feet) but in PROJ format it should always be given in meters. +PROJJSON format is a JSON version of the WKT format, see the [PROJJSON +specification](https://proj.org/en/stable/specifications/projjson.html) + The maximum size of input WKT or PROJ CRS descriptions is limited to 8000 bytes. @@ -116,10 +123,10 @@ Print the CRS information for the current project: g.proj -p ``` -Print the CRS information for the current project in JSON format: +Print the CRS information for the current project in PROJJSON format: ```sh -g.proj -p format=json +g.proj -p format=projjson ``` Print the CRS information for the current project in shell format: @@ -134,19 +141,12 @@ Print the CRS information for the current project in WKT format: g.proj -p format=wkt ``` -Print the CRS information for the current project in PROJ.4 format: +Print the CRS information for the current project in PROJ.4 format (deprecated): ```sh g.proj -p format=proj4 ``` -List the possible datum transformation parameters for the current -project: - -```sh -g.proj -t datumtrans=-1 -``` - ### Create projection (PRJ) file Create a '.prj' file in ESRI format corresponding to the current @@ -240,47 +240,13 @@ Reproject external vector map to current GRASS project using the OGR ogr2ogr -t_srs "`g.proj -wf`" polbnda_italy_GB_ovest.shp polbnda_italy_LL.shp ``` -### Using g.proj JSON output with pandas - -Using the CRS information for the current project in JSON format with pandas: - -```python -import grass.script as gs -import pandas as pd - -# Run g.proj to get CRS information in JSON format. -proj_data = gs.parse_command("g.proj", flags="p", format="json") - -df = pd.DataFrame.from_dict(proj_data, orient='index') -print(df) -``` - -```sh - 0 -name Lambert Conformal Conic -proj lcc -datum nad83 -a 6378137.0 -es 0.006694380022900787 -lat_1 36.16666666666666 -lat_2 34.33333333333334 -lat_0 33.75 -lon_0 -79 -x_0 609601.22 -y_0 0 -no_defs defined -unit Meter -units Meters -meters 1 -``` - ## REFERENCES [PROJ](https://proj.org): Projection/datum support library [GDAL raster library and toolset](https://gdal.org) [OGR vector library and toolset](https://gdal.org/) -Further reading: +### Further reading - [ASPRS Grids and Datum](https://www.asprs.org/asprs-publications/grids-and-datums) diff --git a/general/g.proj/main.c b/general/g.proj/main.c index 0b9f417dceb..d16a58b0afb 100644 --- a/general/g.proj/main.c +++ b/general/g.proj/main.c @@ -235,12 +235,13 @@ int main(int argc, char *argv[]) location->description = _("Name of new project (location) to create"); format = G_define_standard_option(G_OPT_F_FORMAT); - format->options = "plain,shell,json,wkt,proj4"; - format->descriptions = _("plain;Human readable text output;" - "shell;shell script style text output;" - "json;JSON (JavaScript Object Notation);" - "wkt;Well-known text output;" - "proj4;PROJ.4 style text output;"); + format->options = "plain,shell,wkt,projjson,proj4"; + format->descriptions = + _("plain;Human readable text output;" + "shell;shell script style text output;" + "wkt;Well-known text output;" + "projjson;JSON (JavaScript Object Notation) version of WKT;" + "proj4;PROJ.4 style text output;"); format->guisection = _("Print"); G_option_exclusive(printinfo, datuminfo, create, NULL); @@ -250,7 +251,7 @@ int main(int argc, char *argv[]) /* Initialisation & Validation */ - if (strcmp(format->answer, "json") == 0) { + if (strcmp(format->answer, "projjson") == 0) { outputFormat = JSON; } else if (strcmp(format->answer, "shell") == 0) { diff --git a/general/g.proj/output.c b/general/g.proj/output.c index 817eb113235..e1fae7d157f 100644 --- a/general/g.proj/output.c +++ b/general/g.proj/output.c @@ -32,28 +32,32 @@ static int check_xy(enum OutputFormat); static void print_json(G_JSON_Value *); +#if PROJ_VERSION_MAJOR >= 6 +static void print_projjson(void); +#endif + /* print projection information gathered from one of the possible inputs * in GRASS format */ void print_projinfo(enum OutputFormat format) { int i; - G_JSON_Value *value = NULL; - G_JSON_Object *object = NULL; if (check_xy(format)) return; - if (format == PLAIN) + if (format == JSON) { +#if PROJ_VERSION_MAJOR >= 6 + print_projjson(); + + return; +#else + G_fatal_error(_("JSON output is not available.")); +#endif + } + if (format == PLAIN) { fprintf( stdout, "-PROJ_INFO-------------------------------------------------\n"); - else if (format == JSON) { - value = G_json_value_init_object(); - if (value == NULL) { - G_fatal_error( - _("Failed to initialize JSON object. Out of memory?")); - } - object = G_json_object(value); } for (i = 0; i < projinfo->nitems; i++) { @@ -67,17 +71,13 @@ void print_projinfo(enum OutputFormat format) fprintf(stdout, "%-11s: %s\n", projinfo->key[i], projinfo->value[i]); break; - case JSON: - G_json_object_set_string(object, projinfo->key[i], - projinfo->value[i]); - break; case PROJ4: case WKT: + case JSON: break; } } - /* TODO: use projsrid instead */ if (projsrid) { switch (format) { case PLAIN: @@ -88,11 +88,9 @@ void print_projinfo(enum OutputFormat format) case SHELL: fprintf(stdout, "%s=%s\n", "srid", projsrid); break; - case JSON: - G_json_object_set_string(object, "srid", projsrid); - break; case PROJ4: case WKT: + case JSON: break; } } @@ -111,21 +109,14 @@ void print_projinfo(enum OutputFormat format) fprintf(stdout, "%s=%s\n", projunits->key[i], projunits->value[i]); break; - case JSON: - G_json_object_set_string(object, projunits->key[i], - projunits->value[i]); - break; case PROJ4: case WKT: + case JSON: break; } } } - if (format == JSON) { - print_json(value); - } - return; } @@ -361,7 +352,8 @@ static int check_xy(enum OutputFormat format) } object = G_json_object(value); - G_json_object_set_string(object, "name", "xy_location_unprojected"); + G_json_object_set_string(object, "name", + "XY location (unprojected)"); print_json(value); break; @@ -375,7 +367,6 @@ static int check_xy(enum OutputFormat format) return 0; } -/* TODO: use proj_as_projjson() from proj */ void print_json(G_JSON_Value *value) { char *serialized_string = G_json_serialize_to_string_pretty(value); @@ -387,3 +378,60 @@ void print_json(G_JSON_Value *value) G_json_free_serialized_string(serialized_string); G_json_value_free(value); } + +#if PROJ_VERSION_MAJOR >= 6 +void print_projjson(void) +{ + /* PROJ6+: create a PJ object from wkt or srid, + * then get PROJJSON using PROJ API */ + const char *projstr = NULL; + PJ *obj = NULL; + + if (check_xy(PLAIN)) + return; + + if (projwkt) { + obj = proj_create_from_wkt(NULL, projwkt, NULL, NULL, NULL); + } + if (!obj && projsrid) { + obj = proj_create(NULL, projsrid); + } + if (!obj && projepsg) { + int epsg_num; + char *buf = NULL; + + epsg_num = atoi(G_find_key_value("epsg", projepsg)); + if (epsg_num) { + G_asprintf(&buf, "EPSG:%d", epsg_num); + obj = proj_create(NULL, buf); + G_free(buf); + } + } + if (!obj) { + char *outwkt; + + outwkt = GPJ_grass_to_wkt(projinfo, projunits, 0, 0); + /* datum info might be incomplete or incorrect */ + if (outwkt) { + obj = proj_create_from_wkt(NULL, projwkt, NULL, NULL, NULL); + G_free(outwkt); + } + } + if (obj) { + projstr = proj_as_projjson(NULL, obj, NULL); + + if (projstr) + projstr = G_store(projstr); + proj_destroy(obj); + } + + if (projstr) { + fprintf(stdout, "%s\n", projstr); + G_free((char *)projstr); + } + else + G_warning(_("Unable to convert to PROJJSON")); + + return; +} +#endif diff --git a/general/g.proj/testsuite/test_g_proj.py b/general/g.proj/testsuite/test_g_proj.py index f5cad452cbb..23c2947b922 100644 --- a/general/g.proj/testsuite/test_g_proj.py +++ b/general/g.proj/testsuite/test_g_proj.py @@ -113,13 +113,16 @@ def test_shell_output(self): self.assertEqual(result_flag, result_format) - def test_proj_info_output_json(self): + def test_projjson_output(self): """Test if g.proj returns consistent projection info in JSON format.""" # proj has its own PROJJSON format, use this? - module = SimpleModule("g.proj", flags="p", format="json") + module = SimpleModule("g.proj", flags="p", format="projjson") self.assertModule(module) result = json.loads(module.outputs.stdout) - self.assert_keys_in_grass_output(result) + self.assertEqual("ProjectedCRS", result["type"]) + # the base GEOGCRS must be "NAD83(HARN)" with corresponding EPSG code + self.assertEqual("EPSG", result["base_crs"]["id"]["authority"]) + self.assertEqual(4152, result["base_crs"]["id"]["code"]) if __name__ == "__main__": diff --git a/python/grass/app/tests/grass_app_cli_run_pack_test.py b/python/grass/app/tests/grass_app_cli_run_pack_test.py index 8ffe0645941..d50613f6666 100644 --- a/python/grass/app/tests/grass_app_cli_run_pack_test.py +++ b/python/grass/app/tests/grass_app_cli_run_pack_test.py @@ -28,9 +28,9 @@ def test_run_with_crs_as_pack_as_input(pack_raster_file4x5_rows): ) # because we don't set the computational region -@pytest.mark.parametrize("crs", ["EPSG:3358", "EPSG:4326"]) +@pytest.mark.parametrize("epsg_code", [3358, 4326]) @pytest.mark.parametrize("extension", [".grass_raster", ".grr", ".rpack"]) -def test_run_with_crs_as_pack_as_output(tmp_path, crs, extension): +def test_run_with_crs_as_pack_as_output(tmp_path, epsg_code, extension): """Check outputting pack with different CRSs and extensions""" raster = tmp_path / f"test{extension}" subprocess.run( @@ -40,7 +40,7 @@ def test_run_with_crs_as_pack_as_output(tmp_path, crs, extension): "grass.app", "run", "--crs", - crs, + f"EPSG:{epsg_code}", "r.mapcalc.simple", "expression=row() + col()", f"output={raster}", @@ -59,13 +59,15 @@ def test_run_with_crs_as_pack_as_output(tmp_path, crs, extension): str(raster), "g.proj", "-p", - "format=json", + "format=projjson", ], capture_output=True, text=True, check=True, ) - assert json.loads(result.stdout)["srid"] == crs + result_dict = json.loads(result.stdout) + assert result_dict["id"]["authority"] == "EPSG" + assert result_dict["id"]["code"] == epsg_code def test_run_with_crs_as_pack_with_multiple_steps(tmp_path): diff --git a/python/grass/app/tests/grass_app_cli_test.py b/python/grass/app/tests/grass_app_cli_test.py index 1c4a0f087b5..2c8c90a5cca 100644 --- a/python/grass/app/tests/grass_app_cli_test.py +++ b/python/grass/app/tests/grass_app_cli_test.py @@ -62,8 +62,10 @@ def test_subcommand_run_tool_failure_run(): ) def test_subcommand_run_with_crs_as_epsg(capfd): """Check that CRS provided as EPSG is applied""" - assert main(["run", "--crs", "EPSG:3358", "g.proj", "-p", "format=json"]) == 0 - assert json.loads(capfd.readouterr().out)["srid"] == "EPSG:3358" + assert main(["run", "--crs", "EPSG:3358", "g.proj", "-p", "format=projjson"]) == 0 + result_dict = json.loads(capfd.readouterr().out) + assert result_dict["id"]["authority"] == "EPSG" + assert result_dict["id"]["code"] == 3358 def test_subcommand_run_with_crs_as_epsg_subprocess(): @@ -78,13 +80,15 @@ def test_subcommand_run_with_crs_as_epsg_subprocess(): "EPSG:3358", "g.proj", "-p", - "format=json", + "format=projjson", ], capture_output=True, text=True, check=True, ) - assert json.loads(result.stdout)["srid"] == "EPSG:3358" + result_dict = json.loads(result.stdout) + assert result_dict["id"]["authority"] == "EPSG" + assert result_dict["id"]["code"] == 3358 @pytest.mark.skipif( @@ -100,12 +104,14 @@ def test_subcommand_run_with_crs_as_pack(pack_raster_file4x5_rows, capfd): str(pack_raster_file4x5_rows), "g.proj", "-p", - "format=json", + "format=projjson", ] ) == 0 ) - assert json.loads(capfd.readouterr().out)["srid"] == "EPSG:3358" + result_dict = json.loads(capfd.readouterr().out) + assert result_dict["id"]["authority"] == "EPSG" + assert result_dict["id"]["code"] == 3358 def test_subcommand_run_with_crs_as_pack_subprocess(pack_raster_file4x5_rows, capfd): @@ -120,13 +126,15 @@ def test_subcommand_run_with_crs_as_pack_subprocess(pack_raster_file4x5_rows, ca str(pack_raster_file4x5_rows), "g.proj", "-p", - "format=json", + "format=projjson", ], capture_output=True, text=True, check=True, ) - assert json.loads(result.stdout)["srid"] == "EPSG:3358" + result_dict = json.loads(result.stdout) + assert result_dict["id"]["authority"] == "EPSG" + assert result_dict["id"]["code"] == 3358 def test_create_lock_unlock(tmp_path): @@ -199,13 +207,15 @@ def test_create_mapset(tmp_path): str(mapset), "g.proj", "-p", - "format=json", + "format=projjson", ], capture_output=True, text=True, check=True, ) - assert json.loads(result.stdout)["srid"] == "EPSG:3358" + result_dict = json.loads(result.stdout) + assert result_dict["id"]["authority"] == "EPSG" + assert result_dict["id"]["code"] == 3358 # And check that we are really using the newly created mapset, # so the computational region in the default mapset is different. result = subprocess.run( @@ -247,11 +257,11 @@ def test_create_overwrite(tmp_path): assert main(["project", "create", "--overwrite", str(project)]) == 0 -@pytest.mark.parametrize("crs", ["EPSG:4326", "EPSG:3358"]) -def test_create_crs_epsg(tmp_path, crs): - """Check that created project has the requested EPSG""" +@pytest.mark.parametrize("epsg_code", [4326, 3358]) +def test_create_crs_epsg(tmp_path, epsg_code): + """Check that created project has the requested EPSG code""" project = tmp_path / "test_1" - assert main(["project", "create", str(project), "--crs", crs]) == 0 + assert main(["project", "create", str(project), "--crs", f"EPSG:{epsg_code}"]) == 0 result = subprocess.run( [ sys.executable, @@ -262,10 +272,12 @@ def test_create_crs_epsg(tmp_path, crs): str(project), "g.proj", "-p", - "format=json", + "format=projjson", ], capture_output=True, text=True, check=True, ) - assert json.loads(result.stdout)["srid"] == crs + result_dict = json.loads(result.stdout) + assert result_dict["id"]["authority"] == "EPSG" + assert result_dict["id"]["code"] == epsg_code diff --git a/python/grass/grassdb/tests/grass_grassdb_create_from_pack_test.py b/python/grass/grassdb/tests/grass_grassdb_create_from_pack_test.py index 7da4e0396cf..9d543081fdb 100644 --- a/python/grass/grassdb/tests/grass_grassdb_create_from_pack_test.py +++ b/python/grass/grassdb/tests/grass_grassdb_create_from_pack_test.py @@ -23,7 +23,9 @@ def test_path_types( gs.setup.init(path, env=os.environ.copy()) as session, Tools(session=session) as tools, ): - assert tools.g_proj(flags="p", format="json")["srid"] == "EPSG:3358" + result_dict = tools.g_proj(flags="p", format="projjson") + assert result_dict["id"]["authority"] == "EPSG" + assert result_dict["id"]["code"] == 3358 def test_set_crs_in_xy(tmp_path, pack_raster_file4x5_rows): @@ -44,7 +46,9 @@ def test_set_crs_in_xy(tmp_path, pack_raster_file4x5_rows): gs.setup.init(project, env=os.environ.copy()) as session, Tools(session=session) as tools, ): - assert tools.g_proj(flags="p", format="json")["srid"] == "EPSG:3358" + result_dict = tools.g_proj(flags="p", format="projjson") + assert result_dict["id"]["authority"] == "EPSG" + assert result_dict["id"]["code"] == 3358 region = tools.g_region(flags="p", format="json") assert region["crs"]["type"] == "other" assert region["crs"]["type_code"] == 99 diff --git a/python/grass/tools/tests/grass_tools_session_tools_pack_test.py b/python/grass/tools/tests/grass_tools_session_tools_pack_test.py index ac037e52132..c570586b9c6 100644 --- a/python/grass/tools/tests/grass_tools_session_tools_pack_test.py +++ b/python/grass/tools/tests/grass_tools_session_tools_pack_test.py @@ -710,7 +710,9 @@ def test_workflow_create_project_and_run_general_crs( Tools(session=session) as tools, ): assert tools.g_region(flags="p", format="json")["crs"]["type"] == "other" - assert tools.g_proj(flags="p", format="json")["srid"] == "EPSG:3358" + result_dict = tools.g_proj(flags="p", format="projjson") + assert result_dict["id"]["authority"] == "EPSG" + assert result_dict["id"]["code"] == 3358 tools.g_region(raster=ones_raster_file_epsg3358) assert tools.g_region(flags="p", format="json")["cells"] == 4 * 5 tools.r_mapcalc_simple( @@ -738,7 +740,9 @@ def test_workflow_create_project_and_run_ll_crs( Tools(session=session) as tools, ): assert tools.g_region(flags="p", format="json")["crs"]["type"] == "ll" - assert tools.g_proj(flags="p", format="json")["srid"] == "EPSG:4326" + result_dict = tools.g_proj(flags="p", format="projjson") + assert result_dict["id"]["authority"] == "EPSG" + assert result_dict["id"]["code"] == 4326 tools.g_region(raster=ones_raster_file_epsg4326) assert tools.g_region(flags="p", format="json")["cells"] == 4 * 5 tools.r_mapcalc_simple(