diff --git a/.dockerignore b/.dockerignore index c8563e3e96d..3f3883a768b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,7 @@ docker # Do not copy files from previous compilations dist.* -# The following git files are needed by GRASS GIS to extract the revision +# The following git files are needed by GRASS to extract the revision # during compilation. If you are not using one of the Dockerimages from this # repository, delete the .git folder in your Dockerfile after compilation. .git diff --git a/.flake8 b/.flake8 index 06ecd4c5f1a..9a509977289 100644 --- a/.flake8 +++ b/.flake8 @@ -20,7 +20,6 @@ per-file-ignores = # E722 do not use bare 'except' # W605 invalid escape sequence # F821 undefined name 'unicode' - # F841 local variable assigned to but never used # E741 ambiguous variable name 'l' man/build_html.py: E501 man/build_md.py: E501 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 57ad644c0b9..93955414408 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -28,10 +28,10 @@ assignees: '' ## System description +

Attribute management in general

@@ -9,7 +9,7 @@

Attribute management in general

Available drivers

-Available drivers are listed in SQL support in GRASS GIS. +Available drivers are listed in SQL support in GRASS.

Notes:
The DBF driver provides only very limited SQL diff --git a/db/databaseintro.md b/db/databaseintro.md index e86d3d8ecfc..f5b947afca1 100644 --- a/db/databaseintro.md +++ b/db/databaseintro.md @@ -1,9 +1,9 @@ --- -description: Database management in GRASS GIS +description: Database management in GRASS index: database --- -# Database management in GRASS GIS +# Database management in GRASS ## Attribute management in general @@ -14,7 +14,7 @@ map (see [Vector introduction](vectorintro.md)). ## Available drivers -Available drivers are listed in [SQL support in GRASS GIS](sql.md). +Available drivers are listed in [SQL support in GRASS](sql.md). **Notes**: The DBF driver provides only very limited SQL support (as DBF is not an diff --git a/db/db.columns/main.c b/db/db.columns/main.c index fc83ec146dd..de11986c85d 100644 --- a/db/db.columns/main.c +++ b/db/db.columns/main.c @@ -38,8 +38,8 @@ int main(int argc, char **argv) dbTable *table; dbString table_name; int col, ncols; - JSON_Value *root_value = NULL; - JSON_Array *root_array = NULL; + G_JSON_Value *root_value = NULL; + G_JSON_Array *root_array = NULL; parse_command_line(argc, argv); diff --git a/db/db.columns/testsuite/test_dbcolumns.py b/db/db.columns/testsuite/test_dbcolumns.py index 11f30b80df9..bbb42c3d9a9 100644 --- a/db/db.columns/testsuite/test_dbcolumns.py +++ b/db/db.columns/testsuite/test_dbcolumns.py @@ -1,6 +1,6 @@ from grass.gunittest.case import TestCase from grass.gunittest.main import test -from grass.script.core import read_command, parse_command +from grass.script.core import parse_command, read_command output_plain = """cat OBJECTID diff --git a/db/db.connect/db.connect.html b/db/db.connect/db.connect.html index 5b6e2fa1517..3fe87d9ad03 100644 --- a/db/db.connect/db.connect.html +++ b/db/db.connect/db.connect.html @@ -4,7 +4,7 @@

DESCRIPTION

These parameters are then taken as default values by modules so that the user does not need to enter the parameters each time.

-The default database backend in GRASS GIS +The default database backend in GRASS is SQLite (since version 7).

NOTES

diff --git a/db/db.connect/db.connect.md b/db/db.connect/db.connect.md index f1441bf28b3..2b1f7ab45ba 100644 --- a/db/db.connect/db.connect.md +++ b/db/db.connect/db.connect.md @@ -4,7 +4,7 @@ These parameters are then taken as default values by modules so that the user does not need to enter the parameters each time. -The default database backend in GRASS GIS is [SQLite](grass-sqlite.md) +The default database backend in GRASS is [SQLite](grass-sqlite.md) (since version 7). ## NOTES diff --git a/db/db.connect/main.c b/db/db.connect/main.c index 9a7373719d4..a6d6bfdeb49 100644 --- a/db/db.connect/main.c +++ b/db/db.connect/main.c @@ -37,8 +37,8 @@ int main(int argc, char *argv[]) struct Option *driver, *database, *schema, *group, *frmt; struct GModule *module; enum OutputFormat format; - JSON_Value *root_value = NULL; - JSON_Object *root_object = NULL; + G_JSON_Value *root_value = NULL; + G_JSON_Object *root_object = NULL; /* Initialize the GIS calls */ G_gisinit(argv[0]); diff --git a/db/db.connect/testsuite/test_db_connect.py b/db/db.connect/testsuite/test_db_connect.py index e5c4e23885d..2bebefad27f 100644 --- a/db/db.connect/testsuite/test_db_connect.py +++ b/db/db.connect/testsuite/test_db_connect.py @@ -1,6 +1,6 @@ from grass.gunittest.case import TestCase from grass.gunittest.main import test -from grass.script.core import read_command, parse_command +from grass.script.core import parse_command, read_command class TestDbConnect(TestCase): diff --git a/db/db.createdb/db.createdb.html b/db/db.createdb/db.createdb.html index e109c63dcaa..3dc3e4a0863 100644 --- a/db/db.createdb/db.createdb.html +++ b/db/db.createdb/db.createdb.html @@ -13,7 +13,7 @@

EXAMPLES

Create a new SQLite file-based database

-Note that the standard GRASS GIS SQLite database is by default +Note that the standard GRASS SQLite database is by default generated in the user's current mapset. This example shows an out-of-mapset database file creation: diff --git a/db/db.createdb/db.createdb.md b/db/db.createdb/db.createdb.md index a5969b12364..9c974637ffa 100644 --- a/db/db.createdb/db.createdb.md +++ b/db/db.createdb/db.createdb.md @@ -11,7 +11,7 @@ database drivers are supported. ### Create a new SQLite file-based database -Note that the standard GRASS GIS SQLite database is by default generated +Note that the standard GRASS SQLite database is by default generated in the user's current mapset. This example shows an out-of-mapset database file creation: diff --git a/db/db.describe/local_proto.h b/db/db.describe/local_proto.h index 6f7c4b9e682..3be19e3eb0f 100644 --- a/db/db.describe/local_proto.h +++ b/db/db.describe/local_proto.h @@ -1,13 +1,13 @@ #ifndef __LOCAL_PROTO_H__ #define __LOCAL_PROTO_H__ -#include +#include enum OutputFormat { PLAIN, JSON }; -int print_priv(char *, int, enum OutputFormat, JSON_Object *); -int print_column_definition(dbColumn *, int, enum OutputFormat, JSON_Array *); +int print_priv(char *, int, enum OutputFormat, G_JSON_Object *); +int print_column_definition(dbColumn *, int, enum OutputFormat, G_JSON_Array *); int print_table_definition(dbDriver *, dbTable *, enum OutputFormat, - JSON_Object *, JSON_Array *); + G_JSON_Object *, G_JSON_Array *); #endif /* __LOCAL_PROTO_H__ */ diff --git a/db/db.describe/main.c b/db/db.describe/main.c index 33517d8536f..71077fa95b4 100644 --- a/db/db.describe/main.c +++ b/db/db.describe/main.c @@ -19,7 +19,7 @@ #include #include #include -#include +#include #include "local_proto.h" struct { @@ -42,23 +42,23 @@ int main(int argc, char **argv) char buf[1024]; dbString stmt; - JSON_Object *root_object = NULL, *col_object = NULL; - JSON_Value *root_value = NULL, *cols_value = NULL, *col_value = NULL; - JSON_Array *cols_array = NULL; + G_JSON_Object *root_object = NULL, *col_object = NULL; + G_JSON_Value *root_value = NULL, *cols_value = NULL, *col_value = NULL; + G_JSON_Array *cols_array = NULL; parse_command_line(argc, argv); if (parms.format == JSON) { - root_value = json_value_init_object(); + root_value = G_json_value_init_object(); if (root_value == NULL) { G_fatal_error(_("Failed to initialize JSON array. Out of memory?")); } - root_object = json_object(root_value); - cols_value = json_value_init_array(); + root_object = G_json_object(root_value); + cols_value = G_json_value_init_array(); if (cols_value == NULL) { G_fatal_error(_("Failed to initialize JSON array. Out of memory?")); } - cols_array = json_array(cols_value); + cols_array = G_json_array(cols_value); } if (!db_table_exists(parms.driver, parms.database, parms.table)) { @@ -100,8 +100,8 @@ int main(int argc, char **argv) fprintf(stdout, "nrows: %d\n", nrows); break; case JSON: - json_object_set_number(root_object, "ncols", ncols); - json_object_set_number(root_object, "nrows", nrows); + G_json_object_set_number(root_object, "ncols", ncols); + G_json_object_set_number(root_object, "nrows", nrows); break; } @@ -116,32 +116,32 @@ int main(int argc, char **argv) db_get_column_length(column)); break; case JSON: - col_value = json_value_init_object(); - col_object = json_object(col_value); - json_object_set_number(col_object, "position", col + 1); - json_object_set_string(col_object, "name", - db_get_column_name(column)); - json_object_set_string( + col_value = G_json_value_init_object(); + col_object = G_json_object(col_value); + G_json_object_set_number(col_object, "position", col + 1); + G_json_object_set_string(col_object, "name", + db_get_column_name(column)); + G_json_object_set_string( col_object, "type", db_sqltype_name(db_get_column_sqltype(column))); - json_object_set_number(col_object, "length", - db_get_column_length(column)); - json_array_append_value(cols_array, col_value); + G_json_object_set_number(col_object, "length", + db_get_column_length(column)); + G_json_array_append_value(cols_array, col_value); break; } } } if (parms.format == JSON) { - json_object_set_value(root_object, "columns", cols_value); + G_json_object_set_value(root_object, "columns", cols_value); char *serialized_string = NULL; - serialized_string = json_serialize_to_string_pretty(root_value); + serialized_string = G_json_serialize_to_string_pretty(root_value); if (serialized_string == NULL) { G_fatal_error(_("Failed to initialize pretty JSON string.")); } puts(serialized_string); - json_free_serialized_string(serialized_string); - json_value_free(root_value); + G_json_free_serialized_string(serialized_string); + G_json_value_free(root_value); } db_close_database(driver); diff --git a/db/db.describe/printtab.c b/db/db.describe/printtab.c index 4111dfc46df..97bfbe310f9 100644 --- a/db/db.describe/printtab.c +++ b/db/db.describe/printtab.c @@ -1,11 +1,11 @@ #include #include #include "local_proto.h" -#include +#include int print_table_definition(dbDriver *driver, dbTable *table, - enum OutputFormat format, JSON_Object *root_object, - JSON_Array *cols_array) + enum OutputFormat format, G_JSON_Object *root_object, + G_JSON_Array *cols_array) { int ncols, col, nrows; dbColumn *column; @@ -18,9 +18,10 @@ int print_table_definition(dbDriver *driver, dbTable *table, fprintf(stdout, "description:%s\n", db_get_table_description(table)); break; case JSON: - json_object_set_string(root_object, "table", db_get_table_name(table)); - json_object_set_string(root_object, "description", - db_get_table_description(table)); + G_json_object_set_string(root_object, "table", + db_get_table_name(table)); + G_json_object_set_string(root_object, "description", + db_get_table_description(table)); break; } @@ -40,8 +41,8 @@ int print_table_definition(dbDriver *driver, dbTable *table, fprintf(stdout, "nrows:%d\n", nrows); break; case JSON: - json_object_set_number(root_object, "ncols", ncols); - json_object_set_number(root_object, "nrows", nrows); + G_json_object_set_number(root_object, "ncols", ncols); + G_json_object_set_number(root_object, "nrows", nrows); break; } @@ -54,10 +55,10 @@ int print_table_definition(dbDriver *driver, dbTable *table, } int print_column_definition(dbColumn *column, int position, - enum OutputFormat format, JSON_Array *cols_array) + enum OutputFormat format, G_JSON_Array *cols_array) { - JSON_Object *col_object = NULL; - JSON_Value *col_value = NULL; + G_JSON_Object *col_object = NULL; + G_JSON_Value *col_value = NULL; dbString value_string; @@ -82,45 +83,45 @@ int print_column_definition(dbColumn *column, int position, db_test_column_null_allowed(column) ? "yes" : "no"); break; case JSON: - col_value = json_value_init_object(); - col_object = json_object(col_value); - json_object_set_number(col_object, "position", position); - json_object_set_string(col_object, "column", - db_get_column_name(column)); - json_object_set_string(col_object, "description", - db_get_column_description(column)); - json_object_set_string(col_object, "type", - db_sqltype_name(db_get_column_sqltype(column))); - json_object_set_number(col_object, "length", - db_get_column_length(column)); - json_object_set_number(col_object, "scale", - db_get_column_scale(column)); - json_object_set_number(col_object, "precision", - db_get_column_precision(column)); + col_value = G_json_value_init_object(); + col_object = G_json_object(col_value); + G_json_object_set_number(col_object, "position", position); + G_json_object_set_string(col_object, "column", + db_get_column_name(column)); + G_json_object_set_string(col_object, "description", + db_get_column_description(column)); + G_json_object_set_string( + col_object, "type", db_sqltype_name(db_get_column_sqltype(column))); + G_json_object_set_number(col_object, "length", + db_get_column_length(column)); + G_json_object_set_number(col_object, "scale", + db_get_column_scale(column)); + G_json_object_set_number(col_object, "precision", + db_get_column_precision(column)); if (db_test_column_has_default_value(column)) { db_init_string(&value_string); db_convert_column_default_value_to_string(column, &value_string); - json_object_set_string(col_object, "default", - db_get_string(&value_string)); + G_json_object_set_string(col_object, "default", + db_get_string(&value_string)); } else { - json_object_set_null(col_object, "default"); + G_json_object_set_null(col_object, "default"); } - json_object_set_boolean(col_object, "nullok", - db_test_column_null_allowed(column)); + G_json_object_set_boolean(col_object, "nullok", + db_test_column_null_allowed(column)); break; } print_priv("select", db_get_column_select_priv(column), format, col_object); print_priv("update", db_get_column_update_priv(column), format, col_object); if (format == JSON) { - json_array_append_value(cols_array, col_value); + G_json_array_append_value(cols_array, col_value); } return 0; } int print_priv(char *label, int priv, enum OutputFormat format, - JSON_Object *root_object) + G_JSON_Object *root_object) { switch (format) { case PLAIN: @@ -141,13 +142,13 @@ int print_priv(char *label, int priv, enum OutputFormat format, case JSON: switch (priv) { case DB_GRANTED: - json_object_set_boolean(root_object, label, 1); + G_json_object_set_boolean(root_object, label, 1); break; case DB_NOT_GRANTED: - json_object_set_boolean(root_object, label, 0); + G_json_object_set_boolean(root_object, label, 0); break; default: - json_object_set_null(root_object, label); + G_json_object_set_null(root_object, label); break; } break; diff --git a/db/db.dropdb/db.dropdb.html b/db/db.dropdb/db.dropdb.html index a1d1fdf08e3..aee3a5eba0e 100644 --- a/db/db.dropdb/db.dropdb.html +++ b/db/db.dropdb/db.dropdb.html @@ -9,7 +9,7 @@

EXAMPLES

Drop (delete) an existing database connected through SQLite driver

-Note that the standard GRASS GIS SQLite database is by default +Note that the standard GRASS SQLite database is by default found in the user's current mapset. This example shows an out-of-mapset database removal: diff --git a/db/db.dropdb/db.dropdb.md b/db/db.dropdb/db.dropdb.md index 6077ae6ed4e..5a2bd772168 100644 --- a/db/db.dropdb/db.dropdb.md +++ b/db/db.dropdb/db.dropdb.md @@ -8,7 +8,7 @@ ### Drop (delete) an existing database connected through SQLite driver -Note that the standard GRASS GIS SQLite database is by default found in +Note that the standard GRASS SQLite database is by default found in the user's current mapset. This example shows an out-of-mapset database removal: diff --git a/db/db.login/db.login.html b/db/db.login/db.login.html index 6f2eb652e4b..f83cf52ca82 100644 --- a/db/db.login/db.login.html +++ b/db/db.login/db.login.html @@ -54,7 +54,7 @@

SEE ALSO

-SQL support in GRASS GIS +SQL support in GRASS

AUTHOR

diff --git a/db/db.login/db.login.md b/db/db.login/db.login.md index 64ff2e4382d..b9f80dc487e 100644 --- a/db/db.login/db.login.md +++ b/db/db.login/db.login.md @@ -46,7 +46,7 @@ db.login driver=pg database=mydb user=bacava password="" *[db.connect](db.connect.md), [db.test](db.test.md), [db.tables](db.tables.md)* -[SQL support in GRASS GIS](sql.md) +[SQL support in GRASS](sql.md) ## AUTHOR diff --git a/db/drivers/dbf/grass-dbf.html b/db/drivers/dbf/grass-dbf.html index ef7ace3adab..4ec8b25326e 100644 --- a/db/drivers/dbf/grass-dbf.html +++ b/db/drivers/dbf/grass-dbf.html @@ -100,7 +100,7 @@

ERROR MESSAGES

indicates that an unsupported SQL statement (here, 'IN') was used. The only solution is to switch the DBMI backend to a real SQL engine (SQLite, -PostgreSQL, MySQL etc.). See SQL support in GRASS GIS. +PostgreSQL, MySQL etc.). See SQL support in GRASS.

An error message such as: @@ -119,6 +119,6 @@

SEE ALSO

db.connect, -SQL support in GRASS GIS
+SQL support in GRASS
DBF Specifications (Shapelib)
diff --git a/db/drivers/dbf/grass-dbf.md b/db/drivers/dbf/grass-dbf.md index 6d397a1a999..6a813fc4f7a 100644 --- a/db/drivers/dbf/grass-dbf.md +++ b/db/drivers/dbf/grass-dbf.md @@ -100,8 +100,7 @@ SQL parser error: syntax error, unexpected NAME processing 'IN'.. indicates that an unsupported SQL statement (here, 'IN') was used. The only solution is to switch the DBMI backend to a real SQL engine -(SQLite, PostgreSQL, MySQL etc.). See [SQL support in GRASS -GIS](sql.md). +(SQLite, PostgreSQL, MySQL etc.). See [SQL support in GRASS](sql.md). An error message such as: @@ -117,6 +116,6 @@ different column names on the fly. ## SEE ALSO -*[db.connect](db.connect.md), [SQL support in GRASS GIS](sql.md) +*[db.connect](db.connect.md), [SQL support in GRASS](sql.md) [DBF Specifications](http://shapelib.maptools.org/dbf_api.html) (Shapelib)* diff --git a/db/drivers/mysql/grass-mesql.html b/db/drivers/mysql/grass-mesql.html index 3e2299b0d88..9e6224c7644 100644 --- a/db/drivers/mysql/grass-mesql.html +++ b/db/drivers/mysql/grass-mesql.html @@ -62,5 +62,5 @@

SEE ALSO

db.connect, -SQL support in GRASS GIS +SQL support in GRASS diff --git a/db/drivers/mysql/grass-mesql.md b/db/drivers/mysql/grass-mesql.md index 54eada54fba..19773e6d1ca 100644 --- a/db/drivers/mysql/grass-mesql.md +++ b/db/drivers/mysql/grass-mesql.md @@ -62,4 +62,4 @@ Credits: Development of the driver was sponsored by ## SEE ALSO -*[db.connect](db.connect.md), [SQL support in GRASS GIS](sql.md)* +*[db.connect](db.connect.md), [SQL support in GRASS](sql.md)* diff --git a/db/drivers/mysql/grass-mysql.html b/db/drivers/mysql/grass-mysql.html index f86dab367d3..b66fc74024e 100644 --- a/db/drivers/mysql/grass-mysql.html +++ b/db/drivers/mysql/grass-mysql.html @@ -122,5 +122,5 @@

SEE ALSO

db.connect, -SQL support in GRASS GIS +SQL support in GRASS diff --git a/db/drivers/mysql/grass-mysql.md b/db/drivers/mysql/grass-mysql.md index 5258e40e231..be749bef2e6 100644 --- a/db/drivers/mysql/grass-mysql.md +++ b/db/drivers/mysql/grass-mysql.md @@ -109,4 +109,4 @@ Credits: Development of the driver was sponsored by ## SEE ALSO -*[db.connect](db.connect.md), [SQL support in GRASS GIS](sql.md)* +*[db.connect](db.connect.md), [SQL support in GRASS](sql.md)* diff --git a/db/drivers/odbc/grass-odbc.html b/db/drivers/odbc/grass-odbc.html index 75a5ca578d7..4c7891745ff 100644 --- a/db/drivers/odbc/grass-odbc.html +++ b/db/drivers/odbc/grass-odbc.html @@ -160,5 +160,5 @@

SEE ALSO

db.connect, v.db.connect, unixODBC web site, -SQL support in GRASS GIS +SQL support in GRASS diff --git a/db/drivers/odbc/grass-odbc.md b/db/drivers/odbc/grass-odbc.md index c745ebde937..b9b513664cf 100644 --- a/db/drivers/odbc/grass-odbc.md +++ b/db/drivers/odbc/grass-odbc.md @@ -143,4 +143,4 @@ RDBMS. ## SEE ALSO *[db.connect](db.connect.md), [v.db.connect](v.db.connect.md), [unixODBC -web site](https://www.unixodbc.org), [SQL support in GRASS GIS](sql.md)* +web site](https://www.unixodbc.org), [SQL support in GRASS](sql.md)* diff --git a/db/drivers/ogr/execute.c b/db/drivers/ogr/execute.c index 71379aea8e9..8efdb0e9316 100644 --- a/db/drivers/ogr/execute.c +++ b/db/drivers/ogr/execute.c @@ -27,9 +27,10 @@ static int parse_sql_update(const char *, char **, column_info **, int *, int db__driver_execute_immediate(dbString *sql) { - char *where, *table; - int res, ncols, i; - column_info *cols; + char *where = NULL, *table = NULL; + int res, ncols = 0, i; + column_info *cols = NULL; + int ret = DB_FAILED; OGRLayerH hLayer; OGRFeatureH hFeature; @@ -42,22 +43,27 @@ int db__driver_execute_immediate(dbString *sql) /* try RDBMS SQL */ OGR_DS_ExecuteSQL(hDs, db_get_string(sql), NULL, NULL); - if (CPLGetLastErrorType() == CE_None) - return DB_OK; + if (CPLGetLastErrorType() == CE_None) { + ret = DB_OK; + goto cleanup_and_exit; + } /* parse UPDATE statement */ res = parse_sql_update(db_get_string(sql), &table, &cols, &ncols, &where); G_debug(3, "\tUPDATE: table=%s, where=%s, ncols=%d", table, where ? where : "", ncols); - if (res != 0) - return DB_FAILED; + if (res != 0) { + ret = DB_FAILED; + goto cleanup_and_exit; + } /* get OGR layer */ hLayer = OGR_DS_GetLayerByName(hDs, table); if (!hLayer) { db_d_append_error(_("OGR layer <%s> not found"), table); db_d_report_error(); - return DB_FAILED; + ret = DB_FAILED; + goto cleanup_and_exit; } if (where) @@ -71,7 +77,8 @@ int db__driver_execute_immediate(dbString *sql) db_d_append_error(_("Column <%s> not found in table <%s>"), cols[i].name, table); db_d_report_error(); - return DB_FAILED; + ret = DB_FAILED; + goto cleanup_and_exit; } cols[i].qindex = OGR_FD_GetFieldIndex(hFeatureDefn, cols[i].value); hFieldDefn = OGR_FD_GetFieldDefn(hFeatureDefn, cols[i].index); @@ -114,15 +121,18 @@ int db__driver_execute_immediate(dbString *sql) OGR_F_GetFID(hFeature), OGR_L_GetName(hLayer)); OGR_F_Destroy(hFeature); } - + ret = DB_OK; +cleanup_and_exit: + if (cols) { + for (i = 0; i < ncols; i++) { + G_free(cols[i].name); + G_free(cols[i].value); + } + G_free(cols); + } G_free(table); G_free(where); - for (i = 0; i < ncols; i++) { - G_free(cols[i].name); - G_free(cols[i].value); - } - - return DB_OK; + return ret; } int parse_sql_update(const char *sql, char **table, column_info **cols, diff --git a/db/drivers/ogr/grass-ogr.html b/db/drivers/ogr/grass-ogr.html index c573f882288..f921396318f 100644 --- a/db/drivers/ogr/grass-ogr.html +++ b/db/drivers/ogr/grass-ogr.html @@ -6,7 +6,7 @@

SEE ALSO

-SQL support in GRASS GIS +SQL support in GRASS

diff --git a/db/drivers/ogr/grass-ogr.md b/db/drivers/ogr/grass-ogr.md index f7160e2cc60..fb1dc5ae228 100644 --- a/db/drivers/ogr/grass-ogr.md +++ b/db/drivers/ogr/grass-ogr.md @@ -8,6 +8,6 @@ This driver is principally only used by *[v.external](v.external.md)*. ## SEE ALSO -*[SQL support in GRASS GIS](sql.md)* +*[SQL support in GRASS](sql.md)* *[v.external](v.external.md)* diff --git a/db/drivers/postgres/grass-pg.html b/db/drivers/postgres/grass-pg.html index 721d9a035b0..92f8dde21e6 100644 --- a/db/drivers/postgres/grass-pg.html +++ b/db/drivers/postgres/grass-pg.html @@ -134,9 +134,9 @@

SEE ALSO

-Database management in GRASS GIS
+Database management in GRASS
Help pages for database modules
-SQL support in GRASS GIS
+SQL support in GRASS

REFERENCES

diff --git a/db/drivers/postgres/grass-pg.md b/db/drivers/postgres/grass-pg.md index 6d50169faa0..d3d61e5b9e1 100644 --- a/db/drivers/postgres/grass-pg.md +++ b/db/drivers/postgres/grass-pg.md @@ -126,9 +126,9 @@ v.info -t test *[db.connect](db.connect.md), [db.execute](db.execute.md)* -[Database management in GRASS GIS](databaseintro.md) +[Database management in GRASS](databaseintro.md) [Help pages for database modules](database.md) -[SQL support in GRASS GIS](sql.md) +[SQL support in GRASS](sql.md) ## REFERENCES diff --git a/db/drivers/sqlite/grass-sqlite.html b/db/drivers/sqlite/grass-sqlite.html index 57808ccb79c..721634d6104 100644 --- a/db/drivers/sqlite/grass-sqlite.html +++ b/db/drivers/sqlite/grass-sqlite.html @@ -72,7 +72,7 @@

SEE ALSO



- SQL support in GRASS GIS + SQL support in GRASS

diff --git a/db/drivers/sqlite/grass-sqlite.md b/db/drivers/sqlite/grass-sqlite.md index cfd4a155461..aca47fbb1b0 100644 --- a/db/drivers/sqlite/grass-sqlite.md +++ b/db/drivers/sqlite/grass-sqlite.md @@ -53,7 +53,7 @@ sqlitebrowser "$GISDBASE/$LOCATION_NAME/$MAPSET"/sqlite/sqlite.db *[db.connect](db.connect.md), [db.execute](db.execute.md), [db.select](db.select.md)* -*[SQL support in GRASS GIS](sql.md)* +*[SQL support in GRASS](sql.md)* *[SQLite web site](https://www.sqlite.org), [SQLite manual](https://www.sqlite.org/quickstart.html), [sqlite - Management diff --git a/display/d.grid/plot.c b/display/d.grid/plot.c index bee7f9ca6cc..4a029d24e24 100644 --- a/display/d.grid/plot.c +++ b/display/d.grid/plot.c @@ -812,7 +812,7 @@ void check_coords(double e, double n, double *lon, double *lat, int par, *******************************************/ float get_heading(double rows, double cols) { - float azi; + float azi = 0.0; /* NE Quad or due south */ if (rows < 0 && cols <= 0) { diff --git a/display/d.histogram/bar.c b/display/d.histogram/bar.c index 1d5b17d458e..c640cb5f125 100644 --- a/display/d.histogram/bar.c +++ b/display/d.histogram/bar.c @@ -27,8 +27,10 @@ #include -#include #include +#include +#include +#include #include "bar.h" @@ -122,7 +124,10 @@ int bar(struct stat_list *dist_stats, /* list of distribution statistics */ } tic_every = tics[i].every; tic_unit = tics[i].unit; - strcpy(tic_name, tics[i].name); + if (G_strlcpy(tic_name, tics[i].name, sizeof(tic_name)) >= + sizeof(tic_name)) { + G_fatal_error(_("Tic name <%s> is too long"), tics[i].name); + } } else { if (is_fp && !cat_ranges) { @@ -375,7 +380,10 @@ int bar(struct stat_list *dist_stats, /* list of distribution statistics */ i++; tic_every = tics[i].every; tic_unit = tics[i].unit; - strcpy(tic_name, tics[i].name); + if (G_strlcpy(tic_name, tics[i].name, sizeof(tic_name)) >= + sizeof(tic_name)) { + G_fatal_error(_("Tic name <%s> is too long"), tics[i].name); + } stat_start = tic_unit * ((long)(dist_stats->minstat / tic_unit)); stat_finis = tic_unit * ((long)(dist_stats->maxstat / tic_unit)); diff --git a/display/d.labels/do_labels.c b/display/d.labels/do_labels.c index aa3c7d148ac..17a98131aef 100644 --- a/display/d.labels/do_labels.c +++ b/display/d.labels/do_labels.c @@ -223,6 +223,7 @@ int show_it(void) /* get the box size for each line of text and expand the bounding box as * needed */ n_lines = 0; + Y = north - (line_size * 1.2); for (;;) { n_chars = 0; diff --git a/display/d.legend.vect/d.legend.vect.html b/display/d.legend.vect/d.legend.vect.html index 36a2e4e6533..90569c66d2b 100644 --- a/display/d.legend.vect/d.legend.vect.html +++ b/display/d.legend.vect/d.legend.vect.html @@ -33,7 +33,7 @@

Modifying the order of legend entries and omitting certain vector maps have a flag -s which removes the particular vector or thematic vector from vector legend.

The order of entries is defined by the order in Layer Manager (if used -in GRASS GIS GUI). If that's not desired, one can export the legend file +in GRASS GUI). If that's not desired, one can export the legend file into a text file using parameter output, change the order of entries (see section Notes for format description) and then upload the modified file with parameter input. diff --git a/display/d.legend.vect/d.legend.vect.md b/display/d.legend.vect/d.legend.vect.md index ae5da8de687..2fe705326fc 100644 --- a/display/d.legend.vect/d.legend.vect.md +++ b/display/d.legend.vect/d.legend.vect.md @@ -33,7 +33,7 @@ Modules *[d.vect](d.vect.md)* and the particular vector or thematic vector from vector legend. The order of entries is defined by the order in Layer Manager (if used -in GRASS GIS GUI). If that's not desired, one can export the legend file +in GRASS GUI). If that's not desired, one can export the legend file into a text file using parameter **output**, change the order of entries (see section Notes for format description) and then upload the modified file with parameter **input**. Parameter **output** defines path to the diff --git a/display/d.legend.vect/draw.c b/display/d.legend.vect/draw.c index 3de72c7f2a9..4231ce785a1 100644 --- a/display/d.legend.vect/draw.c +++ b/display/d.legend.vect/draw.c @@ -53,6 +53,7 @@ void draw(char *file_name, double LL, double LT, char *title, int cols, /* Draw title */ title_h = 0; title_w = 0; + row_ind = 0.0; if (strlen(title) > 0) { D_font(tit_font); D_text_size(tit_size, tit_size); diff --git a/display/d.linegraph/d.linegraph.html b/display/d.linegraph/d.linegraph.html index a9782c98c2c..d7433442772 100644 --- a/display/d.linegraph/d.linegraph.html +++ b/display/d.linegraph/d.linegraph.html @@ -42,12 +42,12 @@

Managing colors

in the graph. If multiple Y data files are used, an equal number of colors may be used to control the colors of the lines. Colors will be assigned to Y data in respect to the sequence of instantiation on the -command line. It can be one of GRASS GIS named colors or the RGB +command line. It can be one of GRASS named colors or the RGB values from 0-255 separated by colons (RRR:GGG:BBB).

Alternatively, the user can use the color_table option to specify one -of the GRASS GIS predefined color tables. +of the GRASS predefined color tables.

By default, a series of colors will be chosen by the module if none are @@ -60,8 +60,8 @@

Managing colors

The color to be used for titles, axis lines, tics, and scale numbers is determined by the title_color option. The user can provide -one of the GRASS GIS named colors (such as gray, white, or black) -or use the GRASS GIS colon-separated format for RGB (RRR:GGG:BBB). +one of the GRASS named colors (such as gray, white, or black) +or use the GRASS colon-separated format for RGB (RRR:GGG:BBB).

Titles, labels, and tics

@@ -105,7 +105,7 @@

NOTES

Depending on the user's needs, it might be easier or more appropriate to use a 3rd party tool such as xgraph, gnuplot, Matplotlib in Python, or R instead of d.linegraph. -For a more general solution for plotting in GRASS GIS, the user is +For a more general solution for plotting in GRASS, the user is advised to use the d.graph module.

EXAMPLE

diff --git a/display/d.linegraph/d.linegraph.md b/display/d.linegraph/d.linegraph.md index 5ff63568d36..7a72a2d4d04 100644 --- a/display/d.linegraph/d.linegraph.md +++ b/display/d.linegraph/d.linegraph.md @@ -40,11 +40,11 @@ Colors specified by **y_color** option are used for drawing the lines in the graph. If multiple Y data files are used, an equal number of colors may be used to control the colors of the lines. Colors will be assigned to Y data in respect to the sequence of instantiation on the command -line. It can be one of GRASS GIS named colors or the RGB values from +line. It can be one of GRASS named colors or the RGB values from 0-255 separated by colons (RRR:GGG:BBB). Alternatively, the user can use the **color_table** option to specify -one of the GRASS GIS predefined color tables. +one of the GRASS predefined color tables. By default, a series of colors will be chosen by the module if none are provided upon invocation. The order of default colors is red, green, @@ -55,8 +55,8 @@ reproducible results. The color to be used for titles, axis lines, tics, and scale numbers is determined by the **title_color** option. The user can provide one of -the GRASS GIS named colors (such as gray, white, or black) or use the -GRASS GIS colon-separated format for RGB (RRR:GGG:BBB). +the GRASS named colors (such as gray, white, or black) or use the +GRASS colon-separated format for RGB (RRR:GGG:BBB). ### Titles, labels, and tics @@ -94,7 +94,7 @@ multiplied by a factor of 10 before graphing). Depending on the user's needs, it might be easier or more appropriate to use a 3rd party tool such as xgraph, gnuplot, Matplotlib in Python, or R instead of *d.linegraph*. For a more general solution for plotting in -GRASS GIS, the user is advised to use the *[d.graph](d.graph.md)* +GRASS, the user is advised to use the *[d.graph](d.graph.md)* module. ## EXAMPLE diff --git a/display/d.mon/render_cmd.py b/display/d.mon/render_cmd.py index 2959440897e..f3f92ce670d 100644 --- a/display/d.mon/render_cmd.py +++ b/display/d.mon/render_cmd.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 +import glob import os import sys -import glob import tempfile from pathlib import Path +from grass.exceptions import CalledModuleError from grass.script import core as grass from grass.script import task as gtask -from grass.exceptions import CalledModuleError non_rendering_modules = ( "d.colorlist", @@ -73,7 +73,7 @@ def render(cmd, mapfile): grass.run_command(cmd[0], env=env, **cmd[1]) # display driver can generate a blank map file unnecessarily for # non-rendering modules; delete it - if cmd[0] in non_rendering_modules and os.path.exists(mapfile): + if cmd[0] in non_rendering_modules and Path(mapfile).exists(): remove_mapfile(mapfile) except CalledModuleError as e: @@ -198,7 +198,7 @@ def read_stdin(cmd): render(cmd, mapfile) update_cmd_file(os.path.join(path, "cmd"), cmd, mapfile) - if cmd[0] == "d.erase" and os.path.exists(legfile): + if cmd[0] == "d.erase" and Path(legfile).exists(): os.remove(legfile) sys.exit(0) diff --git a/display/d.text/test.py b/display/d.text/test.py index dd8836938a5..66251e744e9 100755 --- a/display/d.text/test.py +++ b/display/d.text/test.py @@ -2,8 +2,8 @@ # Author: Owen Smith - Rewritten from test.pl by Huidae Cho # Run: d.mon start=wx0 && ./test.py | d.text at=0,100 import math -from pathlib import Path import re +from pathlib import Path # Quiet black syntax checking for fonts and colors to keep the code printed to # the display vertically short. diff --git a/display/d.vect/legend.c b/display/d.vect/legend.c index d522332babc..58591361ff2 100644 --- a/display/d.vect/legend.c +++ b/display/d.vect/legend.c @@ -1,4 +1,5 @@ #include +#include #include #include "local_proto.h" @@ -13,7 +14,9 @@ void write_into_legfile(struct Map_info *Map, int type, const char *leglab, char *leg_file; char map[GNAME_MAX]; char *ptr; - strcpy(map, name_map); + if (G_strlcpy(map, name_map, sizeof(map)) >= sizeof(map)) { + G_fatal_error(_("Map name <%s> is too long"), name_map); + } #ifdef _MSC_VER strtok_s(map, "@", &ptr); #else diff --git a/display/d.vect/main.c b/display/d.vect/main.c index 265357a1ee7..27340fe741e 100644 --- a/display/d.vect/main.c +++ b/display/d.vect/main.c @@ -362,7 +362,10 @@ int main(int argc, char **argv) } } - strcpy(map_name, map_opt->answer); + if (G_strlcpy(map_name, map_opt->answer, sizeof(map_name)) >= + sizeof(map_name)) { + G_fatal_error(_("Map name <%s> is too long"), map_opt->answer); + } default_width = atoi(width_opt->answer); if (default_width < 0) diff --git a/doc/Makefile b/doc/Makefile index 27418c77bc9..f068a068e63 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -12,6 +12,7 @@ MDFILES := grass_database.md \ projectionintro.md \ jupyter_intro.md \ interfaces_overview.md \ - python_intro.md + python_intro.md \ + grass_projects.md default: $(patsubst %,$(HTMLDIR)/%,$(FILES)) $(patsubst %,$(MDDIR)/source/%,$(MDFILES)) parsubdirs diff --git a/doc/create_new_project_gui.png b/doc/create_new_project_gui.png new file mode 100644 index 00000000000..f3b0da854e9 Binary files /dev/null and b/doc/create_new_project_gui.png differ diff --git a/doc/examples/raster/r.example/r.example.html b/doc/examples/raster/r.example/r.example.html index 396bb247af4..ce54ae05c52 100644 --- a/doc/examples/raster/r.example/r.example.html +++ b/doc/examples/raster/r.example/r.example.html @@ -1,7 +1,7 @@

DESCRIPTION

r.example does practically do nothing, except -for illustrating GRASS GIS raster programming. It copies +for illustrating GRASS raster programming. It copies over an existing raster map to a new raster map. See the source code for details. diff --git a/doc/grass_database.html b/doc/grass_database.html index dc5393df06e..c5fc8758f02 100644 --- a/doc/grass_database.html +++ b/doc/grass_database.html @@ -1,7 +1,7 @@ - + -A GRASS GIS Database is simply a set of directories and files -with certain structure which GRASS GIS works efficiently with. +A GRASS Database is simply a set of directories and files +with certain structure which GRASS works efficiently with. Project is a directory with data related to one geographic location or a project. All data within one project has the same coordinate reference system. @@ -12,14 +12,14 @@ such as base maps. The PERMANENT mapset also contains metadata related to the project such as the coordinate reference system. -When GRASS GIS is started it connects to a database, project and mapset +When GRASS is started it connects to a database, project and mapset specified by the user.

example: nc_spm - highway - elevation
- Fig. 1: GRASS GIS Database structure as visible to the user + Fig. 1: GRASS Database structure as visible to the user
-

GRASS GIS Database

+

GRASS Database

-All data for GRASS GIS must be in GRASS GIS Database which is a directory +All data for GRASS must be in GRASS Database which is a directory (visible on the disk) containing subdirectories which are GRASS projects. User can have one or more of Databases on the disk. Typically users have one directory called grassdata in their home directory. @@ -45,13 +45,13 @@

GRASS GIS Database

-GRASS GIS Databases can be safely copied or moved as any other directories. -Don't be confused with (relational) databases which are used in GRASS GIS -to hold attribute data and might be part of the GRASS GIS Database. -From user point of view, GRASS GIS Database with all its data in it +GRASS Databases can be safely copied or moved as any other directories. +Don't be confused with (relational) databases which are used in GRASS +to hold attribute data and might be part of the GRASS Database. +From user point of view, GRASS Database with all its data in it is similar to, e.g. PostGIS, database, as it stores all information inside in a specific format and is accessible by specific tools. -GRASS GIS Databases is in GRASS GIS often called GISDBASE or DATABASE. +GRASS Databases is in GRASS often called GISDBASE or DATABASE.

GRASS Projects

@@ -68,7 +68,7 @@

GRASS Projects

GRASS projects can be safely copied or moved as any other directories. Compressed projects are usually what GRASS users exchange between each other when they want to share a lot of data. -For example, GRASS GIS sample data are provided as projects. +For example, GRASS sample data are provided as projects.

Note that a GRASS project used to be called location and @@ -76,7 +76,7 @@

GRASS Projects

Users and programmers familiar with relational databases such as PostgreSQL -can view projects as individual databases inside a storage area (the GRASS GIS Database). +can view projects as individual databases inside a storage area (the GRASS Database). Mapsets in a project are like namespaces or schemas inside a database. @@ -84,13 +84,13 @@

GRASS Projects

GRASS Mapsets

Mapsets contains the actual data, mostly geospatial data, -referred to as maps in GRASS GIS. +referred to as maps in GRASS. Mapsets are a tool for organizing maps in a transparent way as well as a tool for isolating different tasks to prevent data loss.

-GRASS GIS is always connected to one particular mapset. -GRASS GIS modules can create, modify, change, or delete a data only in +GRASS is always connected to one particular mapset. +GRASS modules can create, modify, change, or delete a data only in the current mapset. By default, only the data from the current mapset and PERMANENT mapset are visible. Using @@ -131,11 +131,11 @@

GRASS Mapsets

it is recommended to use r.proj or v.proj to reproject the data. The files and directories should not be moved or modified directly, -but only using GRASS GIS tools. +but only using GRASS tools.

The role of the PERMANENT Mapset

-When creating a new project, GRASS GIS automatically creates a special +When creating a new project, GRASS automatically creates a special mapset called PERMANENT where the core data for the project are stored.

@@ -151,7 +151,7 @@

The role of the PERMANENT Mapset

data (e.g. an elevation model), accessible but write-protected to all users who are working with the same GRASS project as the database owner. To manipulate or add data to PERMANENT, the owner can start -GRASS GIS and choose the relevant project and the PERMANENT mapset. +GRASS and choose the relevant project and the PERMANENT mapset.

The PERMANENT mapset also contains the DEFAULT_WIND file which holds @@ -161,7 +161,7 @@

The role of the PERMANENT Mapset

Importing, exporting and linking data

-GRASS GIS works only with data which are imported into a GRASS Database, +GRASS works only with data which are imported into a GRASS Database, so all data needs to be imported, e.g. by r.in.gdal or highly convenient r.import, @@ -185,18 +185,18 @@

Importing, exporting and linking data

setups a format and directory where the actual data will be stored, however in GRASS Database the data will be created as standard maps. -

Starting GRASS GIS

+

Starting GRASS

-After launching GRASS GIS for the first time, +After launching GRASS for the first time, the GUI opens in a default project world_latlong_wgs84. From there a new project can be created.

- GRASS GIS GUI after first start
+ GRASS GUI after first start
-GRASS GIS can be also started with a given database, project and mapset +GRASS can be also started with a given database, project and mapset from the command line. For example, the following will start in a given mapset with only command line interface: @@ -261,9 +261,9 @@

Creating a New Project with the Project Wizard

See also

-GRASS GIS Reference Manual +GRASS Reference Manual
-GRASS GIS startup program manual page +GRASS startup program manual page
Importing data on GRASS Wiki
diff --git a/doc/grass_projects.md b/doc/grass_projects.md new file mode 100644 index 00000000000..00dcf0cd81d --- /dev/null +++ b/doc/grass_projects.md @@ -0,0 +1,276 @@ +--- +description: GRASS Projects +--- + +# GRASS projects + +GRASS organizes geospatial work into GRASS projects and mapsets. +Each project uses a single coordinate reference system (CRS), +and all data within that project must match it. Inside a project are *mapsets*, +which act as subprojects where data and analyses are organized. +Mapsets can represent different research tasks, study areas, +or the work of individual collaborators. + +## Projects + +A GRASS project is the main organizational unit for geospatial data. +Each project has a single coordinate reference system (CRS), and all data +within it must use that CRS. This ensures that spatial analyses are consistent and +prevents errors that arise when data from different projections are combined. + +A GRASS project organizes geospatial data into one or more mapsets, +which serve as the working environments for analysis. +All geospatial operations in GRASS take place within a mapset. +At minimum, every project includes the PERMANENT mapset, which may store core +baseline data. Additional mapsets can be created for individual tasks, +study areas, or collaborators. + +![Multiple mapsets in a project](project.png) +*Figure: Project raleigh_northcarolina_usa_epsg32119 uses CRS with EPSG code 32119 +and contains three mapsets: PERMANENT, viewsheds, and hydrology* + +A GRASS project is stored as a directory on disk. +Many prefer to keep multiple projects organized in a single directory, +commonly referred to as *GRASS database* and named *grassdata*. +Others store them in separate directories +organized according to their research projects. +GRASS projects can be safely copied, moved, or compressed +(zipped) for sharing or backup, making it easy to transfer an entire project +to another location or collaborator. + +GRASS projects can also be created as temporary, for example when using GRASS +from within another computational environment like R or Python. +Temporary projects exist only for the duration of the session and are deleted +when the session ends. + +!!! grass-tip "Project used to be called location" + In earlier versions of GRASS, projects were referred to as *locations*. + You may still find that term in some older documentation or tutorials, + but it is deprecated. + +## Creating GRASS projects + +When creating a GRASS project, the first step is choosing an appropriate +coordinate reference system (CRS). The CRS should be selected based on the +geographic area of your analysis. Different CRSs are optimized for different +regions and purposes—some preserve angles, others areas or distances—so +choosing the right one ensures accurate results. +For example, a UTM zone is often suitable for a local scale analysis, but it +may be inappropriate for areas that span multiple UTM zones, as distortions +will increase at the edges. + +When creating a project, you can select a CRS using an EPSG code, +derive it from an existing georeferenced file (e.g. a GeoTiff or a GeoPackage), +define a custom CRS, or use a generic cartesian system +for simulations or non-georeferenced data. + +!!! grass-tip "Setting a default region" + The [computational region](g.region.md) defines the spatial extent and + resolution for raster operations in GRASS. + When creating a new project, it is a good idea to set a default computational + region in the PERMANENT mapset, especially when you expect to work with + raster data and multiple mapsets. + The default region is managed in the PERMANENT mapset and applied + automatically to any new mapsets created within the project. + +### Creating a GRASS project from different sources + +GRASS projects can be created from several types of information: + +- Select a CRS using its EPSG code. Search for codes e.g. at [spatialreference.org/explorer.html](https://spatialreference.org/explorer.html) + +- Use a georeferenced raster or vector file (e.g. GeoTiff or GeoPackage) + to define the project's CRS automatically. + +- Create a generic cartesian system (X, Y) for simulations or non-georeferenced data. + +- Specify CRS using a WKT or a PROJ.4 string. + +- Define a CRS manually by specifying its parameters. + +Selecting a CRS from a list of EPSG codes or reading it from a georeferenced +data file are often the most practical and convenient options. + +### Creating a GRASS project in different environments + +Projects can be created in various ways depending on the environment: + +- GRASS Python API: Allows creating a project programmatically with the function + [`create_project`](https://grass.osgeo.org/grass-stable/manuals/libpython/script.html#script.core.create_project). + This example creates a project named *my_project* with a specific EPSG code: + + ```python + import grass.script as gs + + gs.create_project("my_project", epsg="3358") + ``` + +- GRASS [g.proj](g.proj.md) tool: + This tool called from within GRASS session creates a project using + the different sources of CRS information specified above. + This example creates a project named *my_project* using CRS + information from a GeoPackage file *roads.gpkg*: + + ```sh + g.proj georef=roads.gpkg project=my_project + ``` + +- The [grass](grass.md) command: The grass command can be used to create a + project from different sources of CRS information. This example creates an + XY project named *my_project*: + + ```sh + grass -c XY my_project + ``` + + Alternatively, you can create a temporary project: + + ```sh + grass --tmp-project XY + ``` + +- GRASS GUI Project Wizard: Guides you step-by-step to set the CRS and + optionally import a georeferenced file. + + ![Project Wizard](create_new_project_gui.png) + +## Importing and linking data + +Data can be brought into a GRASS project either by importing into GRASS-native +format or by linking external sources. +Importing converts data into GRASS format and reprojects it if necessary +to match the project’s CRS. Imported data are fully managed by GRASS +and remain accessible even if the original source files are moved or deleted. +Importing is recommended in most cases. + +Key GRASS tools for importing raster and vector data include [r.import](r.import.md) +and [v.import](v.import.md). These tools automatically reproject data if needed +and use GDAL library, enabling import of data from a wide range of sources. +There are other import tools for specific file formats, such as +[r.in.pdal](r.in.pdal.md) for lidar data and [r.unpack](r.unpack.md) +and [v.unpack](v.unpack.md) for GRASS native rasters and vectors. + +Linking ([r.external](r.external.md) and [v.external](v.external.md)) +makes external data accessible in GRASS without creating a copy. +This is useful when working with very large datasets or when only a +subset of the data is needed, although it may slow down computations +and has other limitations. + +## Mapsets + +A mapset is a subdirectory within a GRASS project where geospatial analyses +are performed. All mapsets use their project's CRS. +Organizing data into mapsets allows isolation of tasks and supports workflows +where multiple processes or users work on the same project concurrently, +such as in high-performance computing (HPC) or shared network environments. + +Each GRASS project automatically includes one mapset named PERMANENT. +The PERMANENT mapset can be used to store baseline data needed across mapsets. +Additional mapsets can be created for individual subprojects, subregions, or users. +Each mapset contains the actual geospatial data (GRASS uses the term "maps"), +as well as their metadata such as color tables. It also stores mapset-level information +such as the current computational region and raster mask. +Temporary mapsets can also be created for one-off workflows from command line, +Python or R, existing only for the task execution. + +GRASS tools operate only in the current mapset, while data from other mapsets +can be accessed by explicitly providing map's *fully qualified name*, +i.e., map name followed by an '@' sign and the name of the mapset (mapname@mapset). + +!!! grass-tip "Note" + Unlike GRASS projects, mapsets typically *should not be copied or moved + outside of their project directory*. Doing so can corrupt data consistency + unless you are certain of what you are doing. + +### Creating a mapset + +Mapsets can be created in various ways depending on the environment: + +- GRASS [g.mapset](g.mapset.md) tool: + Create a new mapset in the current project and set it as the current mapset: + + ```sh + g.mapset -c newmapset + ``` + +- The [grass](grass.md) command: The grass command can be used to create a mapset, + either temporary for a session or persistent on disk. This example creates + a new mapset in an existing project and starts a GRASS session in that mapset: + + ```sh + grass -c /path/to/my_project/new_mapset + ``` + + Alternatively, you can create a temporary mapset in the current project: + + ```sh + grass --tmp-mapset /path/to/my_project + ``` + +- GRASS GUI: In menu *Settings > GRASS working environment > Create new mapset*. + Or create a new mapset by right-clicking on a project in the [Data Catalog](wxGUI.datacatalog.md). + The new mapset will be created and set as the current mapset. + +!!! grass-tip "Setting computational region" + The computational region defines the spatial extent and resolution for + raster operations in GRASS. Each mapset has its own computational region, + which persists until it is changed by the user with [g.region](g.region.md). + When a new mapset is created, it initially uses a default region stored + in the PERMANENT mapset. If no default region is defined in PERMANENT, + the computational region must be set before any meaningful raster computations + can be performed. + +### Referring to maps from other mapsets + +When you use a map in GRASS, you can either refer to it by its bare name +(e.g., `elevation`) or by its fully qualified name with a mapset (e.g., `elevation@PERMANENT`). + +If you write `elevation@PERMANENT`, GRASS looks in that specified mapset +and nowhere else. If you write only `elevation`, GRASS looks for the map of +that name in the current mapset first. +If it doesn’t find it there, GRASS continues searching in the other mapsets +listed in the *mapset search path*. +By default, the search path always includes the current mapset +(first, highest priority) and the PERMANENT mapset so the baseline +data are always available. + +You can change which mapsets are included in the search path with [g.mapsets](g.mapsets.md). +The search path settings are saved in your current mapset for future sessions. + +### Examples of using projects and mapsets + +**Site suitability analysis for solar plants:** + A data scientist selects an appropriate CRS for the study area and creates + a GRASS project to store base datasets. The PERMANENT mapset contains + a statewide elevation model and land cover map. + For each candidate site, the researcher creates a new mapset, + sets the appropriate computational region, and runs solar radiation computations. + The results are kept separate in their mapsets while the base datasets + remain intact in PERMANENT. + +**Urban growth scenarios:** + A planner is testing several scenarios for urban growth. After creating + a GRASS project, importing data into the PERMANENT mapset and setting default + region, the planner creates multiple mapsets within the same GRASS project, + one per scenario, all using the same computational region and baseline land + use data in PERMANENT. This makes it easy to compare outcomes without + mixing results. + +**Educational setting:** + Each student downloads a copy of a sample dataset containing a GRASS project + with a PERMANENT mapset that stores the base datasets (e.g., elevation and + land cover for a study area). For each assignment, the student creates + a new mapset, keeping their work for different tasks separate and organized. + +**High-performance computing (HPC):** + On a research cluster, the PERMANENT mapset stores a continental digital + elevation model and has a default region set. Individual users create mapsets + that access this data, but their analyses are separated and use different + computational regions. This setup keeps the national dataset safe while allowing + large-scale analyses in parallel. + +**Data pipeline:** + A data engineer needs to combine several raw datasets, run analyses, + and generate summary statistics. They create a temporary GRASS project, + set computational region, perform the workflow, and once it is closed + the project is deleted automatically, leaving no clutter behind. diff --git a/doc/jupyter_intro.md b/doc/jupyter_intro.md index c03ac3bc561..620887782d0 100644 --- a/doc/jupyter_intro.md +++ b/doc/jupyter_intro.md @@ -32,18 +32,22 @@ Now you can import raster or vector data with [r.import](r.import.md) and [v.import](v.import.md). The following examples will use the [North Carolina dataset](https://grass.osgeo.org/download/data/#NorthCarolinaDataset). + !!! grass-tip "Importing packages" - + To import the grass.script and grass.jupyter packages, you need to tell Python where the GRASS Python package is unless you are running a notebook in a GRASS session. - + ```python import sys + import subprocess + sys.path.append( subprocess.check_output(["grass", "--config", "python_path"], text=True).strip() ) ``` + !!! grass-tip "Mapsets" If not specified otherwise in the `gj.init` function, the session will diff --git a/doc/project.png b/doc/project.png new file mode 100644 index 00000000000..9b101797f12 Binary files /dev/null and b/doc/project.png differ diff --git a/doc/project.svg b/doc/project.svg new file mode 100644 index 00000000000..3f7add7f7b1 --- /dev/null +++ b/doc/project.svg @@ -0,0 +1,536 @@ + + + +image/svg+xmlraleigh_northcarolina_usa_epsg32119PERMANENTroadselevationlanduse...EPSG: 32119viewshedsviewpointsviewshed_1viewshed_2streams_1Kbasins_1Kflowaccumulationhydrology...... diff --git a/doc/projectionintro.html b/doc/projectionintro.html index 80a5e1e9ab4..6f995dd55ce 100644 --- a/doc/projectionintro.html +++ b/doc/projectionintro.html @@ -1,4 +1,4 @@ - +

Projection management in general

A GRASS project is referenced with a single projection and coordinate system diff --git a/doc/python_intro.md b/doc/python_intro.md index b3b35720d0d..ff65eca5aa6 100644 --- a/doc/python_intro.md +++ b/doc/python_intro.md @@ -189,61 +189,82 @@ such as *text_split* function and *comma_items* attribute. ### NumPy interface -The GRASS Python API includes a NumPy interface that allows you to read -and write raster data as NumPy arrays. -This makes it easy to integrate GRASS with the broader Python scientific -stack for advanced analysis and custom modeling. -Using *[grass.script.array](https://grass.osgeo.org/grass-stable/manuals/libpython/grass.script.html#script.array.array)* -and *[grass.script.array3d](https://grass.osgeo.org/grass-stable/manuals/libpython/grass.script.html#script.array.array3d)*, -you can switch between GRASS raster maps and NumPy arrays, run GRASS tools, -and perform array-based operations as needed. -It works for rasters as well as for 3D rasters. +The *grass.tools* API allows tools to accept and output NumPy arrays instead of +rasters. Although using GRASS native rasters is faster, NumPy arrays allow for +easy integration with other tools in the Python scientific stack for advanced +analysis and custom modeling. Additional APIs are available to switch between +GRASS raster maps and NumPy arrays, run GRASS tools, and perform array-based +operations. -This example shows a workflow for writing a NumPy array -to a GRASS raster, running a GRASS tool, and loading the result as a NumPy array: +This example shows a workflow which starts with a NumPy array, runs a GRASS tool, +and finishes again with a NumPy array: ```python import numpy as np from grass.tools import Tools -from grass.script import array as garray # Create a 100x100 sinusoidal elevation surface xx, yy = np.meshgrid(np.linspace(0, 1, 100), np.linspace(0, 1, 100)) -elevation_array = np.sin(xx) + np.cos(yy) +elevation = np.sin(xx) + np.cos(yy) # Set the region to match the array dimensions and resolution tools = Tools() -tools.g_region(n=elevation_array.shape[0], s=0, - e=elevation_array.shape[1], w=0, res=1) - -# Write the NumPy array to a new GRASS raster map -map2d = garray.array() -map2d[:] = elevation_array -map2d.write("elevation", overwrite=True) +tools.g_region(n=elevation.shape[0], s=0, e=elevation.shape[1], w=0, res=1) -# Compute e.g., flow accumulation -tools.r_watershed(elevation="elevation", accumulation="accumulation") - -# Load as numpy array -accumulation_array = garray.array("accumulation") +# Use the NumPy array with a GRASS tool to compute, e.g., flow accumulation, +# and get the result as a new NumPy array. +accumulation = tools.r_watershed(elevation=elevation, accumulation=np.array) ``` -This example demonstrates reading an existing GRASS raster into a NumPy array, -modifying the array, and writing the modified array back to a GRASS raster: +Additional APIs, *[grass.script.array](https://grass.osgeo.org/grass-stable/manuals/libpython/grass.script.html#script.array.array)* +and *[grass.script.array3d](https://grass.osgeo.org/grass-stable/manuals/libpython/grass.script.html#script.array.array3d)*, +provide you with full control over read and writing data +between GRASS raster maps and NumPy arrays, including 3D raster maps. +The following example demonstrates reading an existing GRASS raster into a NumPy +array, plotting the data, modifying the array, and writing the modified array +back to a GRASS raster: ```python import numpy as np import seaborn as sns -from grass.script import array as garray +import matplotlib.pyplot as plt +import grass.script.array as ga +from grass.tools import Tools -# Read elevation as numpy array -elev = garray.array(mapname="elevation") +# Set computational region to match the whole raster +tools = Tools() +tools.g_region(raster="elevation") + +# Read elevation as a NumPy array +elevation = ga.array(mapname="elevation") # Plot raster histogram -sns.histplot(data=elev.ravel(), kde=True) +sns.histplot(data=elevation.ravel(), kde=True) +plt.show() # Modify values -elev_2 *= 2 +elevation *= 2 # Write modified array into a GRASS raster -elev_2.write(mapname="elevation_2") +elevation.write(mapname="elevation_2") +``` + +Finally, this example shows a workflow for creating a NumPy array +from scratch and writing it as a GRASS raster: + +```python +import numpy as np +from grass.tools import Tools +import grass.script.array as ga + +# Create a 2x3 constant raster with value 10 +x = np.full((2, 3), 10) + +# Set the region to match the array dimensions and resolution +tools = Tools() +tools.g_region(n=x.shape[0], s=0, e=x.shape[1], w=0, res=1) + +# Write the NumPy array to a new GRASS raster map +map2d = ga.array() +map2d[:] = x +map2d.write("x") # add overwrite=True for repeated runs ``` ## Fine-grained data handling diff --git a/docker/alpine/Dockerfile b/docker/alpine/Dockerfile index f667a23f0df..632fc11c4cc 100644 --- a/docker/alpine/Dockerfile +++ b/docker/alpine/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 as common +FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 as common # Based on: # https://github.com/mundialis/docker-grass-gis/blob/master/Dockerfile @@ -108,7 +108,7 @@ ENV GRASS_CONFIG="\ --without-opengl \ " -# Set environmental variables for GRASS GIS compilation, without debug symbols +# Set environmental variables for GRASS compilation, without debug symbols ENV MYCFLAGS="-O2 -std=gnu99 -m64" \ MYLDFLAGS="-s -Wl,--no-undefined -lblas" \ # CXX stuff: @@ -118,7 +118,7 @@ ENV MYCFLAGS="-O2 -std=gnu99 -m64" \ CXXFLAGS="$MYCXXFLAGS" \ NUMTHREADS=2 -# These packages are required to compile GRASS GIS. +# These packages are required to compile GRASS. ENV GRASS_BUILD_PACKAGES="\ build-base \ bzip2-dev \ @@ -156,11 +156,11 @@ RUN echo "Install main packages";\ apk add --no-cache --virtual .build-deps $GRASS_BUILD_PACKAGES # echo LANG="en_US.UTF-8" > /etc/default/locale; -# Copy and install GRASS GIS +# Copy and install GRASS COPY . /src/grass_build/ WORKDIR /src/grass_build/ -# Configure compile and install GRASS GIS +# Configure compile and install GRASS RUN echo " => Configure and compile grass" && \ /src/grass_build/configure $GRASS_CONFIG && \ make -j $NUMTHREADS && \ @@ -169,7 +169,7 @@ RUN echo " => Configure and compile grass" && \ # Build the GDAL-GRASS plugin # renovate: datasource=github-tags depName=OSGeo/gdal-grass -ARG GDAL_GRASS_VERSION=1.0.4 +ARG GDAL_GRASS_VERSION=2.0.0 RUN git clone --branch $GDAL_GRASS_VERSION --depth 1 https://github.com/OSGeo/gdal-grass.git /src/gdal-grass WORKDIR /src/gdal-grass RUN cmake -B build -DAUTOLOAD_DIR=/usr/lib/gdalplugins -DBUILD_TESTING=OFF && \ @@ -191,7 +191,7 @@ RUN cp /usr/local/grass/gui/wxpython/xml/module_items.xml module_items.xml; \ FROM common AS grass -# GRASS GIS specific +# GRASS specific # allow work with MAPSETs that are not owned by current user ENV GRASS_SKIP_MAPSET_OWNER_CHECK=1 \ SHELL="/bin/bash" \ @@ -200,7 +200,7 @@ ENV GRASS_SKIP_MAPSET_OWNER_CHECK=1 \ LC_ALL="en_US.UTF-8" \ PYTHONPATH="/usr/local/grass/etc/python:$PYTHONPATH" -# Copy GRASS GIS and GDAL GRASS driver from build image +# Copy GRASS and GDAL GRASS driver from build image COPY --from=build /usr/local/bin/grass /usr/local/bin/grass COPY --from=build /usr/local/grass* /usr/local/grass/ COPY --from=build /usr/lib/gdalplugins/*_GRASS.so /usr/lib/gdalplugins/ @@ -210,16 +210,16 @@ ENV GRASS_GISBASE=/usr/local/grass # run simple LAZ test COPY docker/testdata/simple.laz /tmp/ -COPY docker/testdata/test_grass_python.py docker/testdata/test_grass_session.py docker/alpine/grass_tests.sh /scripts/ +COPY docker/testdata/test_grass_session.py docker/alpine/grass_tests.sh /scripts/ -# run GRASS GIS python session and scan the test LAZ file; some cleanup +# run GRASS python session and scan the test LAZ file; some cleanup # also show installed version RUN ln -sf /usr/local/grass /usr/local/grass85; \ rm -rf /tmp/grasstest_epsg_25832; \ $SHELL /scripts/grass_tests.sh; \ python /scripts/test_grass_session.py && rm -rf /tmp/grasstest_epsg_25832; \ grass --tmp-project EPSG:25832 --exec r.in.pdal input="/tmp/simple.laz" output="count_1" method="n" resolution=1 -g \ - && rm -f /scripts/grass_tests.sh /tmp/simple.laz /scripts/test_grass_python.py; \ + && rm -f /scripts/grass_tests.sh /tmp/simple.laz; \ apk del --no-cache gettext pdal-dev; \ grass --tmp-project XY --exec g.version -rge \ && pdal --version \ diff --git a/docker/alpine/README.md b/docker/alpine/README.md index d306cfe0261..fc3de0fd037 100644 --- a/docker/alpine/README.md +++ b/docker/alpine/README.md @@ -1,7 +1,7 @@ -# Docker GRASS GIS (alpine linux) +# Docker GRASS (alpine linux) Dockerfile with an [Alpine Linux](https://www.alpinelinux.org/) image with -[GRASS GIS](https://grass.osgeo.org/), [PDAL](https://pdal.io) support. +[GRASS](https://grass.osgeo.org/), [PDAL](https://pdal.io) support. Download size of this image is only approximately 80 MB. diff --git a/docker/alpine/grass_tests.sh b/docker/alpine/grass_tests.sh index 14c8a735f28..ea9e91cb5a4 100755 --- a/docker/alpine/grass_tests.sh +++ b/docker/alpine/grass_tests.sh @@ -2,9 +2,6 @@ # to be used in alpine Dockerfile -# add dependency -apk add --no-cache py3-scikit-learn - echo "Testing the GDAL-GRASS plugins:" gdalinfo --formats | grep "GRASS Rasters" && \ ogrinfo --formats | grep "GRASS Vectors" || echo "...failed" @@ -14,15 +11,12 @@ ogrinfo --formats | grep "GRASS Vectors" || echo "...failed" # Test PDAL grass --tmp-project EPSG:25832 --exec r.in.pdal input="/tmp/simple.laz" output="count_1" method="n" resolution=1 -g -# Test GRASS GIS Python-addon installation -grass --tmp-project XY --exec g.extension extension=r.learn.ml2 operation=add && \ - grass --tmp-project XY --exec g.extension extension=r.learn.ml2 operation=remove -f +# Test GRASS Python-addon installation +grass --tmp-project XY --exec g.extension extension=r.diversity operation=add && \ + grass --tmp-project XY --exec g.extension extension=r.diversity operation=remove -f -# Test GRASS GIS C-addon installation: raster and vector +# Test GRASS C-addon installation: raster and vector grass --tmp-project XY --exec g.extension extension=r.gwr operation=add && \ grass --tmp-project XY --exec g.extension extension=r.gwr operation=remove -f grass --tmp-project XY --exec g.extension extension=v.centerpoint operation=add && \ grass --tmp-project XY --exec g.extension extension=v.centerpoint operation=remove -f - -# cleanup dependency -apk del py3-scikit-learn diff --git a/docker/debian/Dockerfile b/docker/debian/Dockerfile index e96d0cd0da3..8cc48013191 100644 --- a/docker/debian/Dockerfile +++ b/docker/debian/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bookworm-20240612-slim@sha256:67f3931ad8cb1967beec602d8c0506af1e37e8d73c2a0b38b181ec5d8560d395 +FROM debian:13.2-slim@sha256:18764e98673c3baf1a6f8d960b5b5a1ec69092049522abac4e24a7726425b016 # apt-get update && apt-get install lsb-release -y && lsb_release -a @@ -10,7 +10,7 @@ ENV DEBIAN_FRONTEND=noninteractive # define versions to be used (PDAL is not available on Debian, so we compile it here) # https://github.com/PDAL/PDAL/releases # renovate: datasource=github-tags depName=PDAL/PDAL -ARG PDAL_VERSION=2.7.2 +ARG PDAL_VERSION=2.9.2 SHELL ["/bin/bash", "-c"] @@ -70,7 +70,6 @@ RUN apt-get update && apt-get upgrade -y && \ python3-pip \ python3-setuptools \ python3-venv \ - software-properties-common \ sqlite3 \ subversion \ unzip \ @@ -138,8 +137,8 @@ WORKDIR /src/grass_build # Cleanup potentially leftover GISRC file with wrong path to "demolocation" RUN rm -f /src/grass_build/dist.*/demolocation/.grassrc* -# Set environmental variables for GRASS GIS compilation, without debug symbols -# Set gcc/g++ environmental variables for GRASS GIS compilation, without debug symbols +# Set environmental variables for GRASS compilation, without debug symbols +# Set gcc/g++ environmental variables for GRASS compilation, without debug symbols ENV MYCFLAGS="-O2 -std=gnu99 -m64" ENV MYLDFLAGS="-s" # CXX stuff: @@ -148,7 +147,7 @@ ENV LDFLAGS="$MYLDFLAGS" ENV CFLAGS="$MYCFLAGS" ENV CXXFLAGS="$MYCXXFLAGS" -# Configure compile and install GRASS GIS +# Configure compile and install GRASS ENV NUMTHREADS=4 RUN make distclean || echo "nothing to clean" RUN /src/grass_build/configure \ @@ -183,7 +182,7 @@ ENV LDFLAGS="" ENV CFLAGS="" ENV CXXFLAGS="" -# set SHELL var to avoid /bin/sh fallback in interactive GRASS GIS sessions +# set SHELL var to avoid /bin/sh fallback in interactive GRASS sessions ENV SHELL=/bin/bash ENV LC_ALL="en_US.UTF-8" ENV GRASS_SKIP_MAPSET_OWNER_CHECK=1 @@ -191,10 +190,10 @@ ENV GRASS_SKIP_MAPSET_OWNER_CHECK=1 # https://proj.org/usage/environmentvars.html#envvar-PROJ_NETWORK ENV PROJ_NETWORK=ON -# Create generic GRASS GIS lib name regardless of version number +# Create generic GRASS lib name regardless of version number RUN ln -sf /usr/local/grass85 /usr/local/grass -# show GRASS GIS, PROJ, GDAL etc versions +# show GRASS, PROJ, GDAL etc versions RUN grass --tmp-project EPSG:4326 --exec g.version -rge && \ pdal --version && \ python --version @@ -206,10 +205,10 @@ RUN rm -r /src/grass_build/.git WORKDIR /scripts -# enable GRASS GIS Python session support +# enable GRASS Python session support ## grass --config python-path ENV PYTHONPATH="/usr/local/grass/etc/python:${PYTHONPATH}" -# enable GRASS GIS ctypes imports +# enable GRASS ctypes imports ## grass --config path ENV LD_LIBRARY_PATH="/usr/local/grass/lib:$LD_LIBRARY_PATH" @@ -217,7 +216,7 @@ WORKDIR /tmp COPY docker/testdata/simple.laz . WORKDIR /scripts COPY docker/testdata/test_grass_session.py . -## run GRASS GIS python session and scan the test LAZ file +## run GRASS python session and scan the test LAZ file RUN python /scripts/test_grass_session.py RUN rm -rf /tmp/grasstest_epsg_25832 # test LAZ file diff --git a/docker/debian/README.md b/docker/debian/README.md index d8f7188e47e..e0a5a30fb67 100644 --- a/docker/debian/README.md +++ b/docker/debian/README.md @@ -1,7 +1,7 @@ -# Docker GRASS GIS (Debian Linux) +# Docker GRASS (Debian Linux) Dockerfile with an [Debian Linux](https://www.debian.org/) image with -[GRASS GIS](https://grass.osgeo.org/), [PDAL](https://pdal.io) support. +[GRASS](https://grass.osgeo.org/), [PDAL](https://pdal.io) support. Download size of this image is of approximately 2.6 GB. diff --git a/docker/testdata/test_docker_image.sh b/docker/testdata/test_docker_image.sh index e7cfb006324..59ca0163700 100755 --- a/docker/testdata/test_docker_image.sh +++ b/docker/testdata/test_docker_image.sh @@ -11,29 +11,23 @@ printenv # run simple LAZ test cp docker/testdata/simple.laz /tmp/ cp docker/testdata/test_grass_session.py /tmp/ -cp docker/testdata/test_grass_python.py /tmp/ cp -r demolocation /tmp/ # Test gdal-grass-plugin printf "\n############\nTesting the gdal_grass plugin:\n############\n" gdalinfo --formats | grep "GRASS -raster-" -# Test grass-session -printf "\n############\nTesting grass_session:\n############\n" +# Test grass Python package +printf "\n############\nTesting grass Python package:\n############\n" /usr/bin/python3 /tmp/test_grass_session.py -# Test grass-setup -printf "\n############\nTesting grass script setup:\n############\n" -export DEMOLOCATION=/tmp/demolocation/PERMANENT -/usr/bin/python3 /tmp/test_grass_python.py - # Test PDAL printf "\n############\nTesting PDAL with laz:\n############\n" grass --tmp-project EPSG:25832 --exec r.in.pdal input="/tmp/simple.laz" output="count_1" method="n" resolution=1 -g -# Test GRASS GIS Python-addon installation +# Test GRASS Python-addon installation # add dependency -printf "\n############\nTesting GRASS GIS Python-addon installation:\n############\n" +printf "\n############\nTesting GRASS Python-addon installation:\n############\n" /usr/bin/python3 -m pip install --no-cache-dir scikit-learn grass --tmp-project XY --exec g.extension extension=r.learn.ml2 operation=add && \ @@ -42,14 +36,14 @@ grass --tmp-project XY --exec g.extension extension=r.learn.ml2 operation=add && # cleanup dependency /usr/bin/python3 -m pip uninstall -y scikit-learn -# Test GRASS GIS C-addon installation: raster and vector -printf "\n############\nTesting GRASS GIS C-addon installation:\n############\n" +# Test GRASS C-addon installation: raster and vector +printf "\n############\nTesting GRASS C-addon installation:\n############\n" grass --tmp-project XY --exec g.extension extension=r.gwr operation=add && \ grass --tmp-project XY --exec g.extension extension=r.gwr operation=remove -f grass --tmp-project XY --exec g.extension extension=v.centerpoint operation=add && \ grass --tmp-project XY --exec g.extension extension=v.centerpoint operation=remove -f -# show GRASS GIS, PROJ, GDAL etc versions +# show GRASS, PROJ, GDAL etc versions printf "\n############\nPrinting GRASS, PDAL and Python versions:\n############\n" grass --tmp-project EPSG:4326 --exec g.version -rge && \ pdal --version && \ diff --git a/docker/testdata/test_grass_python.py b/docker/testdata/test_grass_python.py deleted file mode 100644 index 839e46232f7..00000000000 --- a/docker/testdata/test_grass_python.py +++ /dev/null @@ -1,22 +0,0 @@ -# Import GRASS Python bindings -import os -import grass.script as gs - -# hint: do not use ~ as an alias for HOME -with gs.setup.init( - # run in PERMANENT mapset of demolocation in GRASS GIS source - os.environ["DEMOLOCATION"] # "/grassdata/demolocation/PERMANENT", -): - print("grass-setup: tests for PROJ, GDAL, PDAL, GRASS GIS") - print(gs.parse_command("g.gisenv", flags="s")) - - # simple test: just scan the LAZ file - gs.run_command( - "r.in.pdal", - input="/tmp/simple.laz", - output="count_1", - method="n", - flags="g", - resolution=1, - overwrite=True, - ) diff --git a/docker/testdata/test_grass_session.py b/docker/testdata/test_grass_session.py index 1574ba9121c..e9add638747 100644 --- a/docker/testdata/test_grass_session.py +++ b/docker/testdata/test_grass_session.py @@ -1,4 +1,4 @@ -# Import GRASS GIS Python bindings (requires 8.4+) and test r.in.pdal +# Import GRASS Python bindings (requires 8.4+) and test r.in.pdal # PYTHONPATH=$(grass --config python-path) python @@ -10,7 +10,7 @@ # hint: do not use ~ as an alias for HOME with gs.setup.init(project): - print("GRASS GIS session: tests for PROJ, GDAL, PDAL, GRASS GIS") + print("GRASS session: tests for PROJ, GDAL, PDAL, GRASS") print(gs.parse_command("g.gisenv", flags="s")) # simple test: just scan the LAZ file diff --git a/docker/ubuntu/Dockerfile b/docker/ubuntu/Dockerfile index 4950a8c6a77..5a37b4eeb38 100644 --- a/docker/ubuntu/Dockerfile +++ b/docker/ubuntu/Dockerfile @@ -1,160 +1,116 @@ -# syntax=docker/dockerfile:1.17@sha256:38387523653efa0039f8e1c89bb74a30504e76ee9f565e25c9a09841f9427b05 +# syntax=docker/dockerfile:1.20@sha256:26147acbda4f14c5add9946e2fd2ed543fc402884fd75146bd342a7f6271dc1d # Note: This file must be kept in sync in ./Dockerfile and ./docker/ubuntu/Dockerfile. # Changes to this file must be copied over to the other file. - ARG GUI=without -FROM ubuntu:22.04@sha256:1ec65b2719518e27d4d25f104d93f9fac60dc437f81452302406825c46fcc9cb AS common_start +FROM ubuntu:24.04@sha256:c35e29c9450151419d9448b0fd75374fec4fff364a27f176fb458d472dfc9e54 AS common_start + +ARG BASE_NAME="ubuntu:24.04" +ARG PYTHON_VERSION=3.12 +# renovate: datasource=github-tags depName=libgeos/geos +ARG GEOS_VERSION=3.14.1 +# renovate: datasource=github-tags depName=OSGeo/PROJ +ARG PROJ_VERSION=9.7.0 +# renovate: datasource=github-tags depName=OSGeo/gdal +ARG GDAL_VERSION=3.12.0 +# renovate: datasource=github-tags depName=PDAL/PDAL +ARG PDAL_VERSION=2.9.2 +# renovate: datasource=github-tags depName=OSGeo/gdal-grass +ARG GDAL_GRASS_VERSION=2.0.0 -LABEL authors="Carmen Tawalika,Markus Neteler,Anika Weinmann,Stefan Blumentrath" -LABEL maintainer="tawalika@mundialis.de,neteler@mundialis.de,weinmann@mundialis.de" +# Have build parameters as build arguments? +# ARG LDFLAGS="-s -Wl,--no-undefined -lblas" +# ARG CFLAGS="-O2 -std=gnu99" +# ARG CXXFLAGS="" +# ARG PYTHONPATH="$PYTHONPATH" +# ARG LD_LIBRARY_PATH="/usr/local/lib" ENV DEBIAN_FRONTEND=noninteractive SHELL ["/bin/bash", "-c"] -WORKDIR /tmp - -ARG GUI - # Todo: re-consider required dev packages for addons (~400MB in dev packages) ARG GRASS_RUN_PACKAGES="\ - bison \ - build-essential \ - bzip2 \ - curl \ - flex \ - g++ \ - gcc \ - gdal-bin \ - geos-bin \ - git \ - language-pack-en-base \ - libcairo2 \ - libcurl4-gnutls-dev \ - libfftw3-bin \ - libfftw3-dev \ - libfreetype6 \ - libgdal-dev \ - libgeos-dev \ - libgsl-dev \ - libgsl27 \ - libjpeg-turbo8 \ - libjsoncpp-dev \ - liblapacke-dev \ - libmagic-mgc \ - libmagic1 \ - libncurses5 \ - libomp-dev \ - libomp5 \ - libopenblas-base \ - libopenblas-dev \ - libopenjp2-7 \ - libpdal-base13 \ - libpdal-dev \ - libpdal-plugin-hdf \ - libpdal-plugins \ - libpdal-util13 \ - libpnglite0 \ - libpq-dev \ - libpq5 \ - libproj-dev \ - libpython3-all-dev \ - libreadline8 \ - libsqlite3-0 \ - libtiff-tools \ - libzstd1 \ - locales \ - make \ - mesa-utils \ - moreutils \ - ncurses-bin \ - netcdf-bin \ - pdal \ - proj-bin \ - proj-data \ - python-is-python3 \ - python3 \ - python3-dev \ - python3-venv \ - sqlite3 \ - unzip \ - vim \ - wget \ - zip \ - zlib1g \ - " -ENV GRASS_RUN_PACKAGES=${GRASS_RUN_PACKAGES} - -# Define build packages -ARG GRASS_BUILD_PACKAGES="\ - cmake \ - libbz2-dev \ - libcairo2-dev \ - libfreetype6-dev \ - libjpeg-dev \ - libncurses5-dev \ - libnetcdf-dev \ - libopenjp2-7-dev \ - libpnglite-dev \ - libreadline-dev \ - libsqlite3-dev \ - libtiff-dev \ - libzstd-dev \ - mesa-common-dev \ - zlib1g-dev \ - " -ENV GRASS_BUILD_PACKAGES=${GRASS_BUILD_PACKAGES} - -ARG GRASS_CONFIG="\ - --enable-largefile \ - --with-blas \ - --with-bzlib \ - --with-cairo --with-cairo-ldflags=-lfontconfig \ - --with-cxx \ - --with-fftw \ - --with-freetype --with-freetype-includes=/usr/include/freetype2/ \ - --with-gdal=/usr/bin/gdal-config \ - --with-geos \ - --with-lapack \ - --with-netcdf \ - --with-odbc \ - --with-openmp \ - --with-pdal \ - --with-postgres --with-postgres-includes=/usr/include/postgresql \ - --with-proj-share=/usr/share/proj \ - --with-readline \ - --with-sqlite \ - --with-zstd \ - --without-mysql \ - " - -ARG GRASS_PYTHON_PACKAGES="\ - matplotlib \ - numpy \ - Pillow \ - pip \ - psycopg2 \ - python-dateutil \ - python-magic \ - setuptools \ - " -ENV GRASS_PYTHON_PACKAGES=${GRASS_PYTHON_PACKAGES} - - -FROM common_start AS grass_without_gui - -ARG GRASS_CONFIG="${GRASS_CONFIG} --without-opengl" -ENV GRASS_CONFIG=${GRASS_CONFIG} - -FROM common_start AS grass_with_gui + bison \ + build-essential \ + bzip2 \ + curl \ + flex \ + freetds-bin \ + freetds-common \ + g++ \ + gcc \ + git \ + language-pack-en-base \ + libarmadillo12 \ + libcairo2 \ + libcurl4-gnutls-dev \ + libfftw3-bin \ + libfftw3-dev \ + libfreetype6 \ + libfyba0t64 \ + libgeotiff5 \ + libgif7 \ + libgsl-dev \ + libgsl27 \ + libhdf5-dev \ + libjpeg-turbo8 \ + libjson-c5 \ + libjsoncpp-dev \ + libkmlbase1t64 \ + libkmldom1t64 \ + libkmlengine1t64 \ + libkmlxsd1t64 \ + libkmlconvenience1t64 \ + liblapacke-dev \ + liblz4-1 \ + libmagic-mgc \ + libmagic1 \ + libmuparser2v5 \ + libncurses6 \ + libomp-dev \ + libomp5 \ + libopenblas-dev \ + libopenblas0 \ + libopenjp2-7 \ + libpnglite0 \ + libpq-dev \ + libpq5 \ + libpython3-all-dev \ + libreadline8 \ + libspatialite8t64 \ + libsqlite3-0 \ + libsqlite3-mod-spatialite \ + libsvm3 \ + libtiff-tools \ + libxerces-c3.2t64 \ + libzstd1 \ + locales \ + make \ + mesa-utils \ + moreutils \ + ncurses-bin \ + netcdf-bin \ + pcre2-utils \ + python-is-python3 \ + python3 \ + python3-dev \ + sqlite3 \ + tdsodbc \ + unixodbc \ + unzip \ + vim \ + wget \ + zip \ + zlib1g \ +" -ARG GRASS_RUN_PACKAGES="${GRASS_RUN_PACKAGES} \ +ARG GRASS_GUI_RUN_PACKAGES=" \ adwaita-icon-theme-full \ - freeglut3 \ gettext \ libglu1-mesa \ + libglut3.12 \ libgstreamer-plugins-base1.0 \ libgtk-3-0 \ libjpeg8 \ @@ -163,71 +119,80 @@ ARG GRASS_RUN_PACKAGES="${GRASS_RUN_PACKAGES} \ librsvg2-common \ libsdl2-2.0-0 \ libsm6 \ - libtiff5 \ - libwebkit2gtk-4.0 \ + libtiff6 \ + libwebkit2gtk-4.1 \ libxtst6 \ + python3-wxgtk4.0 \ " -# librsvg2-common \ -# (fix error (wxgui.py:7782): Gtk-WARNING **: 19:53:09.774: -# Could not load a pixbuf from /org/gtk/libgtk/theme/Adwaita/assets/check-symbolic.svg. -# This may indicate that pixbuf loaders or the mime database could not be found.) -ARG GRASS_BUILD_PACKAGES="${GRASS_BUILD_PACKAGES} \ - adwaita-icon-theme-full \ - freeglut3-dev \ - libgl1-mesa-dev \ - libglu1-mesa-dev \ - libgstreamer-plugins-base1.0-dev \ - libgtk-3-dev \ +# Define build packages +ARG GRASS_BUILD_PACKAGES="\ + cmake \ + freetds-dev \ + libbz2-dev \ + libarmadillo-dev \ + libcairo2-dev \ + libfreetype6-dev \ + libfyba-dev \ + libgeotiff-dev \ + libgif-dev \ libjpeg-dev \ - libnotify-dev \ - libpng-dev \ - libsdl2-dev \ - libsm-dev \ + libjson-c-dev \ + libkml-dev \ + liblz4-dev \ + libmuparser-dev \ + libncurses-dev \ + libnetcdf-dev \ + libopenjp2-7-dev \ + libpcre2-dev \ + libpnglite-dev \ + libreadline-dev \ + libsqlite3-dev \ + libspatialite-dev \ + libsvm-dev \ libtiff-dev \ - libwebkit2gtk-4.0-dev \ - libxtst-dev \ + libxerces-c-dev \ + libzstd-dev \ + mesa-common-dev \ + ninja-build \ + unixodbc-dev \ + zlib1g-dev \ + swig \ " -ARG GRASS_CONFIG="${GRASS_CONFIG} \ - --with-nls \ - --with-opengl \ - --with-readline \ - --with-x \ - " -ARG GRASS_PYTHON_PACKAGES="${GRASS_PYTHON_PACKAGES} wxPython" +ARG GRASS_PYTHON_PACKAGES="\ + matplotlib \ + numpy \ + packaging>=25.0 \ + Pillow>=10.3.0 \ + psycopg2 \ + python-dateutil \ + python-magic \ + setuptools==80.9.0 \ + cython \ + " + # If you do not use any Gnome Accessibility features, to suppress warning # WARNING **: Couldn't connect to accessibility bus: # execute programs with ENV NO_AT_BRIDGE=1 - -FROM grass_${GUI}_gui AS grass_gis -# Add ubuntugis unstable and fetch packages +# Install runtime-packages +# hadolint ignore=SC2086,DL3008 RUN apt-get update \ - && apt-get install -y --no-install-recommends --no-install-suggests \ - software-properties-common \ - gpg \ - gpg-agent \ - && LC_ALL=C.UTF-8 add-apt-repository -y ppa:ubuntugis/ubuntugis-unstable \ - && apt-get update -y && apt-get upgrade -y \ + && apt-get upgrade -y \ && apt-get install -y --no-install-recommends --no-install-suggests \ $GRASS_RUN_PACKAGES \ - && apt-get remove -y gpg gpg-agent \ && apt-get autoremove -y \ && apt-get clean all \ && rm -rf /var/lib/apt/lists/* -RUN echo LANG="en_US.UTF-8" > /etc/default/locale \ - && echo en_US.UTF-8 UTF-8 >> /etc/locale.gen \ - && locale-gen - ## fetch vertical datums for PDAL and store into PROJ dir -WORKDIR /src +# WORKDIR /src # # Get datum grids # # Currently using https://proj.org/en/9.3/usage/network.html#how-to-enable-network-capabilities -# FROM ubuntu:22.04 AS datum_grids +# FROM ubuntu:24.04 AS datum_grids # # See: https://github.com/OSGeo/PROJ-data # RUN apt-get update \ @@ -237,121 +202,310 @@ WORKDIR /src # && rm -rf /var/lib/apt/lists/* # WORKDIR /tmp -# RUN wget --no-check-certificate -r -l inf -A tif https://cdn.proj.org/ +# RUN wget -q --no-check-certificate -r -l inf -A tif https://cdn.proj.org/ # Start build stage -FROM grass_gis AS build +FROM common_start AS build_common # Add build packages +# hadolint ignore=SC2086,DL3008,DL3013 RUN apt-get update \ - && apt-get install -y --no-install-recommends --no-install-suggests \ - $GRASS_BUILD_PACKAGES \ + && apt-get install -y --no-install-recommends --no-install-suggests \ + ${GRASS_BUILD_PACKAGES} ca-certificates \ + && apt-get autoremove -y \ && apt-get clean all \ - && rm -rf /var/lib/apt/lists/* - -# Add pip and Python packages -RUN (echo "Install Python" \ - && wget https://bootstrap.pypa.io/pip/get-pip.py \ - # && apt-get install -y python3-ensurepip \ - && python get-pip.py \ - # && python3 -m ensurepip --upgrade \ + && echo "Install Python" \ + && wget --progress="dot:giga" https://bootstrap.pypa.io/pip/get-pip.py \ + && python get-pip.py --break-system-packages \ && rm -r get-pip.py \ - && mkdir -p /src/site-packages \ - && cd /src \ - && python -m pip install --no-cache-dir -t /src/site-packages --upgrade \ - $GRASS_PYTHON_PACKAGES \ + # Deactivate python3-packaging to allow installation of newer version + && apt-get remove -y python3-packaging \ + && apt-get clean all \ + && python3 -m pip install --break-system-packages --no-cache-dir --upgrade \ + ${GRASS_PYTHON_PACKAGES} \ + # Add back dev-packages that depend on python3-packaging + && apt-get update \ + && apt-get install -y --no-install-recommends --no-install-suggests \ + libcairo2-dev libglib2.0-dev libglib2.0-dev-bin \ + # Clean up + && apt-get autoremove -y \ + && apt-get clean all \ + && rm -rf /var/lib/apt/lists/* \ && rm -r /root/.cache \ - && rm -rf /tmp/pip-* \ - ) + && rm -rf /tmp/pip-* + +# Set environmental variables for GRASS compilation, without debug symbols +# Set gcc/g++ environmental variables for GRASS compilation, without debug symbols +ARG NUMTHREADS=4 +ENV CMAKE_BUILD_PARALLEL_LEVEL=$NUMTHREADS +WORKDIR /src + +COPY ./utils/build_ubuntu_dependencies.sh /src/ + +# Build and install PROJ, GEOS, GDAL and PDAL from source +FROM build_common AS build_proj +RUN /src/build_ubuntu_dependencies.sh proj "$PROJ_VERSION" "$NUMTHREADS" + +FROM build_common AS build_geos +RUN /src/build_ubuntu_dependencies.sh geos "$GEOS_VERSION" "$NUMTHREADS" + +FROM build_common AS build_gdal_pdal +COPY --link --from=build_proj /usr/local /usr/local +COPY --link --from=build_geos /usr/local /usr/local + +# hadolint ignore=DL3059 +RUN /src/build_ubuntu_dependencies.sh gdal "$GDAL_VERSION" "$NUMTHREADS" + +# hadolint ignore=DL3059 +RUN /src/build_ubuntu_dependencies.sh pdal "$PDAL_VERSION" "$NUMTHREADS" + +# With all the libraries needed compiled from source, now build grass and gdal-grass +FROM build_gdal_pdal AS build_grass_config + +ARG GRASS_CONFIG="\ + --enable-largefile \ + --with-blas \ + --with-bzlib \ + --with-cairo --with-cairo-ldflags=-lfontconfig --with-cairo-includes=/usr/include/cairo/ \ + --with-cxx \ + --with-fftw \ + --with-freetype --with-freetype-includes=/usr/include/freetype2/ \ + --with-gdal=/usr/local/bin/gdal-config \ + --with-geos \ + --with-lapack \ + --with-libsvm \ + --with-netcdf \ + --with-odbc \ + --with-openmp \ + --with-pcre \ + --with-pdal \ + --with-postgres --with-postgres-includes=/usr/include/postgresql \ + --with-proj-share=/usr/local/share/proj \ + --with-readline \ + --with-sqlite \ + --with-zstd \ + --without-mysql \ +" + +ARG GRASS_NO_GUI_CONFIG="--without-nls --without-opengl --without-x" + +ARG GRASS_GUI_CONFIG="\ + --with-nls \ + --with-opengl \ + --with-x \ +" + +# librsvg2-common \ +# (fix error (wxgui.py:7782): Gtk-WARNING **: 19:53:09.774: +# Could not load a pixbuf from /org/gtk/libgtk/theme/Adwaita/assets/check-symbolic.svg. +# This may indicate that pixbuf loaders or the mime database could not be found.) +ARG GRASS_GUI_BUILD_PACKAGES=" \ + freeglut3-dev \ + libgl1-mesa-dev \ + libglu1-mesa-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libjpeg-dev \ + libnotify-dev \ + libpng-dev \ + libsdl2-dev \ + libsm-dev \ + libtiff-dev \ + libwebkit2gtk-4.1-dev \ + libxtst-dev \ +" -# copy grass gis source COPY . /src/grass_build/ +# copy grass source WORKDIR /src/grass_build -# Set environmental variables for GRASS GIS compilation, without debug symbols -# Set gcc/g++ environmental variables for GRASS GIS compilation, without debug symbols -ENV MYCFLAGS="-O2 -std=gnu99" -ENV MYLDFLAGS="-s" -# CXX stuff: -ENV LD_LIBRARY_PATH="/usr/local/lib" -ENV LDFLAGS="$MYLDFLAGS" -ENV CFLAGS="$MYCFLAGS" -ENV CXXFLAGS="$MYCXXFLAGS" - -# Configure compile and install GRASS GIS -ENV NUMTHREADS=4 -RUN make distclean || echo "nothing to clean" -RUN ./configure $GRASS_CONFIG \ +ENV LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \ + LDFLAGS="-s -Wl,--no-undefined -lblas" \ + CFLAGS="-O2 -std=gnu99" \ + CXXFLAGS="" \ + NUMTHREADS=$NUMTHREADS + +FROM build_grass_config AS build_grass_with_gui + +ARG GRASS_CONFIG="$GRASS_CONFIG $GRASS_GUI_CONFIG" +ARG GRASS_GUI_PACKAGES="$GRASS_GUI_RUN_PACKAGES $GRASS_GUI_BUILD_PACKAGES" + +# hadolint ignore=DL3008 +RUN echo "Installing GRASS GUI packages: $GRASS_GUI_PACKAGES" \ + && apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y --no-install-recommends --no-install-suggests \ + $GRASS_GUI_PACKAGES \ + && python3 -m pip install -U --break-system-packages --no-cache-dir --upgrade \ + -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-24.04 \ + wxpython \ + # Clean up + && pip cache purge \ + && apt-get autoremove -y \ + && apt-get clean all \ + && rm -rf /var/lib/apt/lists/* \ + # Need to generate locale when NLS is enabled + && echo LANG="en_US.UTF-8" > /etc/default/locale \ + && echo en_US.UTF-8 UTF-8 >> /etc/locale.gen \ + && locale-gen + +FROM build_grass_config AS build_grass_without_gui + +ARG GRASS_CONFIG="$GRASS_CONFIG $GRASS_NO_GUI_CONFIG" + +# hadolint ignore=DL3006 +FROM build_grass_${GUI}_gui AS build_grass + +# Configure compile and install GRASS +# hadolint ignore=SC2086,DL3008 +RUN make -j $NUMTHREADS distclean || echo "nothing to clean" \ + && ./configure $GRASS_CONFIG \ && make -j $NUMTHREADS \ && make install && ldconfig \ - && cp /usr/local/grass85/gui/wxpython/xml/module_items.xml module_items.xml; \ - rm -rf /usr/local/grass85/demolocation; \ - rm -rf /usr/local/grass85/fonts; \ - rm -rf /usr/local/grass85/gui; \ - rm -rf /usr/local/grass85/share; \ - mkdir -p /usr/local/grass85/gui/wxpython/xml/; \ - mv module_items.xml /usr/local/grass85/gui/wxpython/xml/module_items.xml; - + && rm -rf /usr/local/grass85/demolocation \ + && cp /usr/local/grass85/gui/wxpython/xml/module_items.xml module_items.xml + +FROM build_grass AS build_grass_with_gui_built +RUN echo "GUI selected, skipping GUI related cleanup" + +FROM build_grass AS build_grass_without_gui_built +RUN echo "No GUI selected, removing GUI related files" \ + && rm -rf /usr/local/grass85/fonts \ + && rm -rf /usr/local/grass85/gui \ + && rm -rf /usr/local/grass85/docs/html \ + && rm -rf /usr/local/grass85/docs/mkdocs \ + && rm -rf /usr/local/grass85/share + +# hadolint ignore=DL3006 +FROM build_grass_${GUI}_gui_built AS build_grass_plugin # Build the GDAL-GRASS plugin -# renovate: datasource=github-tags depName=OSGeo/gdal-grass -ARG GDAL_GRASS_VERSION=1.0.4 -RUN git clone --branch $GDAL_GRASS_VERSION --depth 1 https://github.com/OSGeo/gdal-grass.git \ +# hadolint ignore=DL3003,DL3059 +RUN git clone --branch "$GDAL_GRASS_VERSION" --depth 1 https://github.com/OSGeo/gdal-grass.git \ && cd "gdal-grass" \ - && cmake -B build -DAUTOLOAD_DIR=/usr/lib/gdalplugins -DBUILD_TESTING=OFF \ + && cmake -B build -DAUTOLOAD_DIR=/usr/local/lib/gdalplugins -DBUILD_TESTING=OFF \ && cmake --build build \ && cmake --install build \ && cd /src \ && rm -rf "gdal-grass" +ENV GRASS_SKIP_MAPSET_OWNER_CHECK=1 \ + SHELL="/bin/bash" \ + # https://proj.org/usage/environmentvars.html#envvar-PROJ_NETWORK + PROJ_NETWORK=ON \ + LC_ALL="en_US.UTF-8" \ + PYTHONPATH="/usr/local/grass/etc/python" \ + LD_LIBRARY_PATH="/usr/local/grass/lib:/usr/local/lib" \ + GDAL_DRIVER_PATH="/usr/local/lib/gdalplugins" + +ENV LD_LIBRARY_PATH="/usr/local/grass/lib:/usr/local/lib:/usr/lib" + +# show GRASS, PROJ, GDAL etc versions +RUN /usr/local/bin/grass --tmp-project EPSG:4326 --exec g.version -rge && \ + pdal --version && \ + python3 --version && \ + python3 -c "import numpy; print('numpy', numpy.__version__)" && \ + python3 -c "import matplotlib as mpl; print('matplotlib', mpl.__version__)" && \ + python3 -c "from osgeo import gdal; print('GDAL_GRASS driver:', 'available' if gdal.GetDriverByName('GRASS') else 'not-available')" + # Leave build stage -FROM grass_gis AS grass_gis_final +FROM common_start AS grass_final_without_gui +RUN echo "No additional steps needed without GUI." + +FROM common_start AS grass_final_with_gui -# GRASS GIS specific +# hadolint ignore=SC2086,DL3008 +RUN apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y --no-install-recommends --no-install-suggests \ + $GRASS_GUI_RUN_PACKAGES \ + && apt-get autoremove -y \ + && apt-get clean all \ + && rm -rf /var/lib/apt/lists/* + +# hadolint ignore=DL3006 +FROM grass_final_${GUI}_gui AS grass_final + +# Set with "$(git branch --show-current)" +ARG BRANCH=main +# Set with "$(git log -n 1 --pretty=format:'%h')" +ARG REVISION=latest +# Set with "$(head include/VERSION -n 3 | sed ':a;N;$!ba;s/\n/ /g')" +ARG VERSION=8.5.dev +# Set with "$(date +'%Y-%m-%dT%H:%M:%S%:z')" +ARG BUILD_DATE +ARG TITLE="GRASS ${VERSION}" +ARG URL="https://github.com/OSGeo/grass/tree/${BRANCH}/docker" +ARG DOCUMENTATION="https://github.com/OSGeo/grass/tree/${BRANCH}/docker/README.md" +ARG SOURCE="https://github.com/OSGeo/grass/tree/${BRANCH}/docker/ubuntu" +ARG AUTHORS="Carmen Tawalika,Markus Neteler,Anika Weinmann,Stefan Blumentrath" +ARG MAINTAINERS="tawalika@mundialis.de,neteler@mundialis.de,weinmann@mundialis.de" +ARG VENDOR="GRASS Development Team" +ARG LICENSE="GPL-2.0-or-later" +ARG DESCRIPTION="GRASS (Geographic Resources Analysis Support System, https://grass.osgeo.org/) " +ARG DESCRIPTION="$DESCRIPTION is a powerful computational engine for geospatial processing." +ARG DESCRIPTION="$DESCRIPTION Build with:" +ARG DESCRIPTION="$DESCRIPTION PROJ ${PROJ_VERSION}," +ARG DESCRIPTION="$DESCRIPTION GEOS ${GEOS_VERSION}," +ARG DESCRIPTION="$DESCRIPTION GDAL ${GDAL_VERSION}," +ARG DESCRIPTION="$DESCRIPTION PDAL ${PDAL_VERSION}." +ARG REF_NAME="grass" + +# Add OCI annotations (see: https://github.com/opencontainers/image-spec/blob/main/annotations.md) +LABEL org.opencontainers.image.authors="$AUTHORS" \ + org.opencontainers.image.vendor="$VENDOR" \ + org.opencontainers.image.licenses="$LICENSE" \ + org.opencontainers.image.created="$BUILD_DATE" \ + org.opencontainers.image.revision="$REVISION" \ + org.opencontainers.image.url="$URL" \ + org.opencontainers.image.source="$SOURCE" \ + org.opencontainers.image.version="$VERSION" \ + org.opencontainers.image.title="$TITLE" \ + org.opencontainers.image.description="$DESCRIPTION" \ + org.opencontainers.image.base.name="$BASE_NAME" \ + org.opencontainers.image.ref.name="$REF_NAME" \ + org.opencontainers.image.documentation="$DOCUMENTATION" \ + maintainers="$MAINTAINERS" + +# GRASS specific # allow work with MAPSETs that are not owned by current user ENV GRASS_SKIP_MAPSET_OWNER_CHECK=1 \ SHELL="/bin/bash" \ # https://proj.org/usage/environmentvars.html#envvar-PROJ_NETWORK PROJ_NETWORK=ON \ LC_ALL="en_US.UTF-8" \ - PYTHONPATH="/usr/local/grass/etc/python/:${PYTHONPATH}" \ - LD_LIBRARY_PATH="/usr/local/grass/lib:$LD_LIBRARY_PATH" \ - GDAL_DRIVER_PATH="/usr/lib/gdalplugins" - -# Copy GRASS GIS from build image -COPY --link --from=build /usr/local/bin/* /usr/local/bin/ -COPY --link --from=build /usr/local/grass85 /usr/local/grass85/ -COPY --link --from=build /src/site-packages /usr/lib/python3.10/ -COPY --link --from=build /usr/lib/gdalplugins /usr/lib/gdalplugins + PYTHONPATH="/usr/local/grass/etc/python" \ + LD_LIBRARY_PATH="/usr/local/grass/lib:/usr/local/lib:/usr/lib" \ + GDAL_DRIVER_PATH="/usr/local/lib/gdalplugins" + +# Copy GRASS, GDAL-GRASS-plugin and compiled dependencies from build image +COPY --link --from=build_grass_plugin /usr/local /usr/local +COPY --link --from=build_grass_plugin /src/grass_build/module_items.xml /usr/local/grass85/gui/wxpython/xml/module_items.xml # COPY --link --from=datum_grids /tmp/cdn.proj.org/*.tif /usr/share/proj/ -# Create generic GRASS GIS lib name regardless of version number -RUN ln -sf /usr/local/grass85 /usr/local/grass +# Create generic GRASS lib name regardless of version number +RUN ln -sf /usr/local/grass85 /usr/local/grass \ + && ldconfig -# show GRASS GIS, PROJ, GDAL etc versions +# show GRASS, PROJ, GDAL etc versions RUN grass --tmp-project EPSG:4326 --exec g.version -rge && \ pdal --version && \ - python --version - -WORKDIR /scripts - -# enable GRASS GIS Python session support -## grass --config python-path -ENV PYTHONPATH="/usr/local/grass/etc/python:${PYTHONPATH}" -# enable GRASS GIS ctypes imports -## grass --config path -ENV LD_LIBRARY_PATH="/usr/local/grass/lib:$LD_LIBRARY_PATH" + python3 --version && \ + python3 -c "import numpy; print('numpy', numpy.__version__)" && \ + python3 -c "import matplotlib as mpl; print('matplotlib', mpl.__version__)" && \ + python3 -c "import packaging; print('packaging', packaging.__version__)" && \ + python3 -c "from osgeo import gdal; print('GDAL_GRASS driver:', 'available' if gdal.GetDriverByName('GRASS') else 'not-available')" WORKDIR /tmp COPY docker/testdata/simple.laz . WORKDIR /scripts COPY docker/testdata/test_grass_session.py . -## run GRASS GIS python session and scan the test LAZ file -RUN python /scripts/test_grass_session.py -RUN rm -rf /tmp/grasstest_epsg_25832 -# test LAZ file -RUN grass --tmp-project EPSG:25832 --exec r.in.pdal input="/tmp/simple.laz" output="count_1" method="n" resolution=1 -g +# Run GRASS python session and scan the test LAZ file +RUN python3 /scripts/test_grass_session.py \ + && rm -rf /tmp/grasstest_epsg_25832 \ + && grass --tmp-project EPSG:25832 --exec r.in.pdal input="/tmp/simple.laz" output="count_1" method="n" resolution=1 -g \ + && rm /tmp/simple.laz \ + && rm /scripts/test_grass_session.py + WORKDIR /grassdb VOLUME /grassdb - -CMD ["$GRASSBIN", "--version"] diff --git a/docker/ubuntu/README.md b/docker/ubuntu/README.md index a8f2764a239..59677a3ab3d 100644 --- a/docker/ubuntu/README.md +++ b/docker/ubuntu/README.md @@ -1,118 +1,129 @@ -# Docker GRASS GIS (Ubuntu Linux) +# Docker GRASS (Ubuntu Linux) # -Dockerfile with an [Ubuntu Linux](https://ubuntu.com/) image with -[GRASS GIS](https://grass.osgeo.org/), [PDAL](https://pdal.io) support. +## Using Ubuntu docker builds maintained by the GRASS development team ## -Download size of this image is of approximately 2.6 GB. +Note: If the images are not found locally on your computer, +they will be downloaded from docker-hub (~ 2.6 GB). -Clone this repository and change directory: +__Run the command line image for the development branch with__: ```bash -git clone https://github.com/OSGeo/grass.git -cd grass +docker run -it osgeo/grass-gis:main-ubuntu grass -h ``` -## Ubuntu stable - -__Build the docker with__: +__Run the command line image for releasebranch_8_4 with__: ```bash -docker build \ - --file docker/ubuntu/Dockerfile \ - --tag grass-py3-pdal:stable-ubuntu . +docker run -it osgeo/grass-gis:releasebranch_8_4-ubuntu grass -h ``` -View the images available using `sudo docker images` and open a bash terminal -with: +__Run the image for the development branch with GUI support with__: ```bash -$ docker run -it grass-py3-pdal:stable-ubuntu /bin/bash -bash-5.0# +docker run -it osgeo/grass-gis:main-ubuntu_wxgui grass -h ``` -__To build a stable version__: +## Building and running Ubuntu docker images ## -change to the releasebranch or tag you want to build: +Dockerfile with an [Ubuntu Linux](https://ubuntu.com/) image with +[GRASS](https://grass.osgeo.org/), [PDAL](https://pdal.io) support. -```bash -git checkout remotes/origin/releasebranch_8_2 -``` +Download size of this image is of approximately 2.6 GB. -and build and enter with: +To build your own image, do as follows: -```bash -$ docker build \ - -f docker/ubuntu/Dockerfile \ - -t grass-py3-pdal:stable-ubuntu . +Clone this repository and change directory: -$ docker run -it grass-py3-pdal:stable-ubuntu /bin/bash -bash-5.0# +```bash +git clone https://github.com/OSGeo/grass.git +cd grass ``` -## Ubuntu latest - -__Build the docker with__: +__Build the development branch with__: ```bash -$ docker build \ - --file docker/ubuntu/Dockerfile_ubuntu_latest_pdal \ - --tag grass-py3-pdal:latest-ubuntu . +DOCKER_BUILDKIT=1 docker build . \ + --file docker/ubuntu/Dockerfile \ + --tag grass-main-ubuntu:latest \ + --build-arg GUI=without \ + --build-arg NUMTHREADS=8 \ + --build-arg BRANCH="$(git branch --show-current)" \ + --build-arg REVISION="$(git log -n 1 --pretty=format:'%h')" \ + --build-arg VERSION="$(head include/VERSION -n 3 | sed ':a;N;$!ba;s/\n/ /g')" \ + --build-arg BUILD_DATE="$(date +'%Y-%m-%dT%H:%M:%S%:z')" ``` View the images available using `sudo docker images` and open a bash terminal with: ```bash -$ docker run -it grass-py3-pdal:latest-ubuntu /bin/bash -bash-5.0# +docker run -it grass-main-ubuntu:latest grass -h ``` -__To build a latest version__: +__To build a stable version__: -change to the releasebranch or tag you want to build: +Change to the releasebranch or tag you want to build: ```bash -git checkout remotes/origin/releasebranch_8_2 +git checkout remotes/origin/releasebranch_8_4 ``` and build and enter with: ```bash -$ docker build \ - -f docker/ubuntu/Dockerfile_ubuntu_latest_pdal \ - -t grass-py3-pdal:latest-ubuntu . - -$ docker run -it grass-py3-pdal:latest-ubuntu /bin/bash -bash-5.0# +DOCKER_BUILDKIT=1 docker build . \ + --file docker/ubuntu/Dockerfile \ + --tag grass-stable-ubuntu:latest \ + --build-arg GUI=without \ + --build-arg NUMTHREADS=8 \ + --build-arg BRANCH="$(git branch --show-current)" \ + --build-arg REVISION="$(git log -n 1 --pretty=format:'%h')" \ + --build-arg VERSION="$(head include/VERSION -n 3 | sed ':a;N;$!ba;s/\n/ /g')" \ + --build-arg BUILD_DATE="$(date +'%Y-%m-%dT%H:%M:%S%:z')" + +docker run -it grass-stable-ubuntu:latest grass -h ``` -__To build a latest version with wxgui__: +__To build and run a latest version with wxgui__: The `GUI` build argument allows choosing if the GUI should be included in the build (`GUI=with`) or not (`GUI=without`). ```bash -$ DOCKER_BUILDKIT=1 docker build \ - --file docker/ubuntu/Dockerfile \ - --tag grass-main-ubuntu-wxgui:latest \ - --build-arg GUI=with . +git checkout main +DOCKER_BUILDKIT=1 docker build . \ + --file docker/ubuntu/Dockerfile \ + --tag grass-main-ubuntu-wxgui:latest \ + --build-arg GUI=with \ + --build-arg NUMTHREADS=8 \ + --build-arg BRANCH="$(git branch --show-current)" \ + --build-arg REVISION="$(git log -n 1 --pretty=format:'%h')" \ + --build-arg VERSION="$(head include/VERSION -n 3 | sed ':a;N;$!ba;s/\n/ /g')" \ + --build-arg BUILD_DATE="$(date +'%Y-%m-%dT%H:%M:%S%:z')" + +xhost local:$(id -u) +docker run -it --privileged --user=$(id -u):$(id -g) --rm \ + --volume="$(pwd)/:/data" --volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" \ + --env HOME=/data/ --env DISPLAY=$DISPLAY \ + --device="/dev/dri/card0:/dev/dri/card0" \ + grass-main-ubuntu-wxgui:latest grass --gui ``` -## Test the docker image +## Test the docker image ## -Note: Adjust the volume mount to the path of the GRASS GIS source code directory. +Note: Adjust the volume mount to the path of the GRASS source code directory. ```bash # Test basic functionality -$ docker run -ti \ - -v /opt/src/grass:/grassdb \ - grass-py3-pdal:latest-ubuntu \ - bash docker/testdata/test_docker_image.sh +docker run -ti \ + -v /opt/src/grass:/grassdb \ + grass-main-ubuntu:latest \ + bash docker/testdata/test_docker_image.sh # Execute also the full test suite -$ docker run -ti \ - -e TESTSUITE=1 \ - -v /opt/src/grass:/grassdb \ - grass-py3-pdal:latest-ubuntu \ - bash docker/testdata/test_docker_image.sh +docker run -ti \ + -e TESTSUITE=1 \ + -v /opt/src/grass:/grassdb \ + grass-main-ubuntu:latest \ + bash docker/testdata/test_docker_image.sh ``` diff --git a/docker/ubuntu_wxgui/Dockerfile b/docker/ubuntu_wxgui/Dockerfile index ad50a65c92b..c62c5fc02c9 100644 --- a/docker/ubuntu_wxgui/Dockerfile +++ b/docker/ubuntu_wxgui/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:22.04@sha256:1ec65b2719518e27d4d25f104d93f9fac60dc437f81452302406825c46fcc9cb +FROM ubuntu:24.04@sha256:c35e29c9450151419d9448b0fd75374fec4fff364a27f176fb458d472dfc9e54 LABEL authors="Carmen Tawalika,Markus Neteler,Anika Weinmann,Tomas Zigo" LABEL maintainer="tawalika@mundialis.de,neteler@mundialis.de,weinmann@mundialis.de" @@ -8,7 +8,7 @@ ENV DEBIAN_FRONTEND=noninteractive # define versions to be used (PDAL is not available on Ubuntu/Debian, so we compile it here) # https://github.com/PDAL/PDAL/releases # renovate: datasource=github-tags depName=PDAL/PDAL -ARG PDAL_VERSION=2.7.1 +ARG PDAL_VERSION=2.9.2 SHELL ["/bin/bash", "-c"] @@ -47,7 +47,7 @@ RUN apt-get update && apt-get upgrade -y && \ libcurl4-gnutls-dev \ libfftw3-bin \ libfftw3-dev \ - libfreetype6-dev \ + libfreetype-dev \ libgdal-dev \ libgeos-dev \ libgl1-mesa-dev \ @@ -57,11 +57,11 @@ RUN apt-get update && apt-get upgrade -y && \ libgtk-3-dev \ libjpeg-dev \ libjsoncpp-dev \ - libncurses5-dev \ + libncurses-dev \ libnetcdf-dev \ libnotify4 \ - libopenblas-base \ libopenblas-dev \ + libopenblas0 \ libopenjp2-7 \ libopenjp2-7-dev \ libpnglite-dev \ @@ -70,8 +70,8 @@ RUN apt-get update && apt-get upgrade -y && \ libpython3-all-dev \ libreadline-dev \ librsvg2-common \ - libsecret-1-0 \ libsdl2-2.0-0 \ + libsecret-1-0 \ libsqlite3-dev \ libtiff-dev \ libxtst6 \ @@ -106,9 +106,9 @@ RUN apt-get update && apt-get upgrade -y && \ && apt-get clean all && rm -rf /var/lib/apt/lists/* # wxGUI -RUN pip install -U \ - -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04 \ - wxPython \ +RUN pip install -U --break-system-packages \ + -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-24.04 \ + wxpython \ && pip cache purge # If you do not use any Gnome Accessibility features, to suppress warning # WARNING **: Couldn't connect to accessibility bus: @@ -174,8 +174,8 @@ WORKDIR /src/grass_build # Cleanup potentially leftover GISRC file with wrong path to "demolocation" RUN rm -f /src/grass_build/dist.*/demolocation/.grassrc* -# Set environmental variables for GRASS GIS compilation, without debug symbols -# Set gcc/g++ environmental variables for GRASS GIS compilation, without debug symbols +# Set environmental variables for GRASS compilation, without debug symbols +# Set gcc/g++ environmental variables for GRASS compilation, without debug symbols ENV MYCFLAGS="-O2 -std=gnu99 -m64" ENV MYLDFLAGS="-s" # CXX stuff: @@ -184,7 +184,7 @@ ENV LDFLAGS="$MYLDFLAGS" ENV CFLAGS="$MYCFLAGS" ENV CXXFLAGS="$MYCXXFLAGS" -# Configure compile and install GRASS GIS +# Configure compile and install GRASS # wxGUI require # --with-nls # --with-x @@ -224,7 +224,7 @@ ENV LDFLAGS="" ENV CFLAGS="" ENV CXXFLAGS="" -# set SHELL var to avoid /bin/sh fallback in interactive GRASS GIS sessions +# set SHELL var to avoid /bin/sh fallback in interactive GRASS sessions ENV SHELL=/bin/bash ENV LC_ALL="en_US.UTF-8" ENV GRASS_SKIP_MAPSET_OWNER_CHECK=1 @@ -232,25 +232,24 @@ ENV GRASS_SKIP_MAPSET_OWNER_CHECK=1 # https://proj.org/usage/environmentvars.html#envvar-PROJ_NETWORK ENV PROJ_NETWORK=ON -# Create generic GRASS GIS lib name regardless of version number +# Create generic GRASS lib name regardless of version number RUN ln -sf /usr/local/grass85 /usr/local/grass -# show GRASS GIS, PROJ, GDAL etc versions +# show GRASS, PROJ, GDAL etc versions RUN grass --tmp-project EPSG:4326 --exec g.version -rge && \ pdal --version && \ python --version # Reduce the image size -RUN apt-get autoremove -y -RUN apt-get clean -y +RUN apt-get autoremove -y && apt-get clean -y RUN rm -r /src/grass_build/.git WORKDIR /scripts -# enable GRASS GIS Python session support +# enable GRASS Python session support ## grass --config python-path ENV PYTHONPATH="/usr/local/grass/etc/python:${PYTHONPATH}" -# enable GRASS GIS ctypes imports +# enable GRASS ctypes imports ## grass --config path ENV LD_LIBRARY_PATH="/usr/local/grass/lib:$LD_LIBRARY_PATH" @@ -258,7 +257,7 @@ WORKDIR /tmp COPY docker/testdata/simple.laz . WORKDIR /scripts COPY docker/testdata/test_grass_session.py . -## run GRASS GIS python session and scan the test LAZ file +## run GRASS python session and scan the test LAZ file RUN python /scripts/test_grass_session.py RUN rm -rf /tmp/grasstest_epsg_25832 # test LAZ file diff --git a/docker/ubuntu_wxgui/README.md b/docker/ubuntu_wxgui/README.md index 8840afd81d3..0718e10e8ee 100644 --- a/docker/ubuntu_wxgui/README.md +++ b/docker/ubuntu_wxgui/README.md @@ -1,7 +1,7 @@ -# Docker GRASS GIS (Ubuntu Linux) +# Docker GRASS (Ubuntu Linux) Dockerfile with an [Ubuntu Linux](https://ubuntu.com/) image with -[GRASS GIS](https://grass.osgeo.org/), [PDAL](https://pdal.io) support. +[GRASS](https://grass.osgeo.org/), [PDAL](https://pdal.io) support. Download size of this image is of approximately 2.6 GB. diff --git a/flake.lock b/flake.lock index 2b5eaa6ef91..671ac905f5a 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1753121425, - "narHash": "sha256-TVcTNvOeWWk1DXljFxVRp+E0tzG1LhrVjOGGoMHuXio=", + "lastModified": 1763759067, + "narHash": "sha256-LlLt2Jo/gMNYAwOgdRQBrsRoOz7BPRkzvNaI/fzXi2Q=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "644e0fc48951a860279da645ba77fe4a6e814c5e", + "rev": "2cccadc7357c0ba201788ae99c4dfa90728ef5e0", "type": "github" }, "original": { @@ -19,11 +19,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1753432016, - "narHash": "sha256-cnL5WWn/xkZoyH/03NNUS7QgW5vI7D1i74g48qplCvg=", + "lastModified": 1764527385, + "narHash": "sha256-nA5ywiGKl76atrbdZ5Aucd8SjF/v8ew9b9QsC+MKL14=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6027c30c8e9810896b92429f0092f624f7b1aace", + "rev": "23258e03aaa49b3a68597e3e50eb0cbce7e42e9d", "type": "github" }, "original": { @@ -35,11 +35,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1751159883, - "narHash": "sha256-urW/Ylk9FIfvXfliA1ywh75yszAbiTEVgpPeinFyVZo=", + "lastModified": 1761765539, + "narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "14a40a1d7fb9afa4739275ac642ed7301a9ba1ab", + "rev": "719359f4562934ae99f5443f20aa06c2ffff91fc", "type": "github" }, "original": { diff --git a/general/g.filename/g.filename.html b/general/g.filename/g.filename.html index 5462f5cb48e..f2fae9505fd 100644 --- a/general/g.filename/g.filename.html +++ b/general/g.filename/g.filename.html @@ -5,7 +5,7 @@

DESCRIPTION

vector and site maps, region definitions and imagery groups.

The list of element names to search for is not fixed; any subdirectory of the mapset directory is a valid element name. -

However, the user can find the list of standard GRASS GIS element names in +

However, the user can find the list of standard GRASS element names in the file $GISBASE/etc/element_list. This is the file which g.remove/g.rename/g.copy use to determine which files need to be deleted/renamed/copied for a given entity type. diff --git a/general/g.filename/g.filename.md b/general/g.filename/g.filename.md index 832ff234fe8..212651baa9c 100644 --- a/general/g.filename/g.filename.md +++ b/general/g.filename/g.filename.md @@ -7,7 +7,7 @@ vector and site maps, region definitions and imagery groups. The list of element names to search for is not fixed; any subdirectory of the mapset directory is a valid element name. -However, the user can find the list of standard GRASS GIS element names +However, the user can find the list of standard GRASS element names in the file `$GISBASE/etc/element_list`. This is the file which g.remove/g.rename/g.copy use to determine which files need to be deleted/renamed/copied for a given entity type. diff --git a/general/g.findfile/main.c b/general/g.findfile/main.c index d09913c72ba..37f4efb7bfa 100644 --- a/general/g.findfile/main.c +++ b/general/g.findfile/main.c @@ -28,7 +28,7 @@ enum OutputFormat { SHELL, JSON }; -void print_json(JSON_Value *root_value) +void print_json(G_JSON_Value *root_value) { char *json_string = G_json_serialize_to_string_pretty(root_value); if (!json_string) { @@ -54,8 +54,8 @@ int main(int argc, char *argv[]) struct Flag *n_flag, *l_flag, *t_flag; size_t len; enum OutputFormat format; - JSON_Value *root_value = NULL; - JSON_Object *root_object = NULL; + G_JSON_Value *root_value = NULL; + G_JSON_Object *root_object = NULL; module = G_define_module(); G_add_keyword(_("general")); diff --git a/general/g.findfile/tests/test_g_findfile.py b/general/g.findfile/tests/test_g_findfile.py index 9297ad2468e..a0b83e63d60 100644 --- a/general/g.findfile/tests/test_g_findfile.py +++ b/general/g.findfile/tests/test_g_findfile.py @@ -1,8 +1,8 @@ import os -import pytest from pathlib import Path import grass.script as gs +import pytest @pytest.fixture(scope="module") diff --git a/general/g.gisenv/g.gisenv.html b/general/g.gisenv/g.gisenv.html index bf95770390c..2cc4607ed97 100644 --- a/general/g.gisenv/g.gisenv.html +++ b/general/g.gisenv/g.gisenv.html @@ -1,6 +1,6 @@

DESCRIPTION

-g.gisenv outputs and modifies the user's current GRASS GIS +g.gisenv outputs and modifies the user's current GRASS variable settings. When a user runs GRASS, certain variables are set specifying the GRASS diff --git a/general/g.gisenv/g.gisenv.md b/general/g.gisenv/g.gisenv.md index dc7cd056bca..964ed22fa30 100644 --- a/general/g.gisenv/g.gisenv.md +++ b/general/g.gisenv/g.gisenv.md @@ -1,6 +1,6 @@ ## DESCRIPTION -*g.gisenv* outputs and modifies the user's current GRASS GIS variable +*g.gisenv* outputs and modifies the user's current GRASS variable settings. When a user runs GRASS, certain variables are set specifying the GRASS data base, project, mapset, peripheral device drivers, etc., being used in the current GRASS session. These variable name settings diff --git a/general/g.gui/g.gui.html b/general/g.gui/g.gui.html index 3988edb68e3..7d42deae047 100644 --- a/general/g.gui/g.gui.html +++ b/general/g.gui/g.gui.html @@ -5,7 +5,7 @@

DESCRIPTION

default User Interface (UI) settings.

-GRASS GIS comes with both a wxPython-based GUI +GRASS comes with both a wxPython-based GUI aka wxGUI (ui=wxpython) and command line text-based UI (ui=text). diff --git a/general/g.gui/g.gui.md b/general/g.gui/g.gui.md index 469b79c7a76..e24e689cd25 100644 --- a/general/g.gui/g.gui.md +++ b/general/g.gui/g.gui.md @@ -4,7 +4,7 @@ The *g.gui* module allows user to start the Graphical User Interface (GUI) from the command line prompt or to change the default User Interface (UI) settings. -GRASS GIS comes with both a wxPython-based GUI aka *[wxGUI](wxGUI.md)* +GRASS comes with both a wxPython-based GUI aka *[wxGUI](wxGUI.md)* (**ui=wxpython**) and command line text-based UI (**ui=text**). ## NOTES diff --git a/general/g.list/list.c b/general/g.list/list.c index 5da09913b3a..64eae71f933 100644 --- a/general/g.list/list.c +++ b/general/g.list/list.c @@ -75,12 +75,12 @@ void print_list(FILE *fp, struct elist *el, int count, const char *separator, { int i; char fullname[1024]; - JSON_Array *root_array = NULL; - JSON_Value *root_value = NULL; - JSON_Object *map_object = NULL; - JSON_Value *map_value = NULL; + G_JSON_Array *root_array = NULL; + G_JSON_Value *root_value = NULL; + G_JSON_Object *map_object = NULL; + G_JSON_Value *map_value = NULL; - if (!count) + if (!count && format != JSON) return; if (format == JSON) { @@ -91,7 +91,8 @@ void print_list(FILE *fp, struct elist *el, int count, const char *separator, root_array = G_json_array(root_value); } - qsort(el, count, sizeof(struct elist), compare_elist); + if (count > 1) + qsort(el, count, sizeof(struct elist), compare_elist); for (i = 0; i < count; i++) { int need_mapset = 0; diff --git a/general/g.list/tests/g_list_test.py b/general/g.list/tests/g_list_test.py index 5e76e9844bb..b0019e1ef69 100644 --- a/general/g.list/tests/g_list_test.py +++ b/general/g.list/tests/g_list_test.py @@ -1,7 +1,10 @@ -import grass.script as gs -import pytest import sys +import pytest + +import grass.script as gs +from grass.tools import Tools + def test_default_output(simple_dataset): """Test default output of g.list.""" @@ -290,3 +293,15 @@ def test_json_output(simple_dataset): }, ] assert actual == expected + + +def test_no_json_output(simple_dataset): + """Test g.list returning an empty JSON list""" + tools = Tools(env=simple_dataset.env) + result = tools.g_list( + type="raster", + pattern="does_not_exist", + format="json", + ) + assert not result + assert list(result) == [] diff --git a/general/g.mapset/main.c b/general/g.mapset/main.c index 07eddafb061..22ad9b1829f 100644 --- a/general/g.mapset/main.c +++ b/general/g.mapset/main.c @@ -17,29 +17,30 @@ * **************************************************************/ +#include #include #include #include #include #include #include -#include +#include #include enum OutputFormat { PLAIN, JSON }; // Function to serialize and print JSON value -static void serialize_and_print_json_object(JSON_Value *root_value) +static void serialize_and_print_json_object(G_JSON_Value *root_value) { - char *serialized_string = json_serialize_to_string_pretty(root_value); + char *serialized_string = G_json_serialize_to_string_pretty(root_value); if (!serialized_string) { - json_value_free(root_value); + G_json_value_free(root_value); G_fatal_error(_("Failed to serialize JSON to pretty format.")); } puts(serialized_string); - json_free_serialized_string(serialized_string); - json_value_free(root_value); + G_json_free_serialized_string(serialized_string); + G_json_value_free(root_value); } int main(int argc, char *argv[]) @@ -60,8 +61,8 @@ int main(int argc, char *argv[]) const char *shell; char path[GPATH_MAX]; enum OutputFormat format; - JSON_Object *root_object = NULL; - JSON_Value *root_value = NULL; + G_JSON_Object *root_object = NULL; + G_JSON_Value *root_value = NULL; G_gisinit(argv[0]); @@ -112,12 +113,12 @@ int main(int argc, char *argv[]) if (strcmp(opt.format->answer, "json") == 0) { format = JSON; - root_value = json_value_init_object(); + root_value = G_json_value_init_object(); if (root_value == NULL) { G_fatal_error( _("Failed to initialize JSON object. Out of memory?")); } - root_object = json_object(root_value); + root_object = G_json_object(root_value); } else { format = PLAIN; @@ -135,8 +136,8 @@ int main(int argc, char *argv[]) break; case JSON: - json_object_set_string(root_object, "project", location_old); - json_object_set_string(root_object, "mapset", mapset_old); + G_json_object_set_string(root_object, "project", location_old); + G_json_object_set_string(root_object, "mapset", mapset_old); serialize_and_print_json_object(root_value); break; } @@ -160,22 +161,22 @@ int main(int argc, char *argv[]) if (flag.list->answer) { char **ms; int nmapsets; - JSON_Array *mapsets_array = NULL; - JSON_Value *mapsets_value = NULL; + G_JSON_Array *mapsets_array = NULL; + G_JSON_Value *mapsets_value = NULL; G_setenv_nogisrc("LOCATION_NAME", location_new); G_setenv_nogisrc("GISDBASE", gisdbase_new); ms = G_get_available_mapsets(); if (format == JSON) { - mapsets_value = json_value_init_array(); + mapsets_value = G_json_value_init_array(); if (mapsets_value == NULL) { G_fatal_error( _("Failed to initialize JSON array. Out of memory?")); } - mapsets_array = json_array(mapsets_value); + mapsets_array = G_json_array(mapsets_value); - json_object_set_string(root_object, "project", location_new); + G_json_object_set_string(root_object, "project", location_new); } for (nmapsets = 0; ms[nmapsets]; nmapsets++) { @@ -186,7 +187,7 @@ int main(int argc, char *argv[]) break; case JSON: - json_array_append_string(mapsets_array, ms[nmapsets]); + G_json_array_append_string(mapsets_array, ms[nmapsets]); break; } } @@ -197,7 +198,7 @@ int main(int argc, char *argv[]) break; case JSON: - json_object_set_value(root_object, "mapsets", mapsets_value); + G_json_object_set_value(root_object, "mapsets", mapsets_value); serialize_and_print_json_object(root_value); break; } @@ -279,7 +280,10 @@ int main(int argc, char *argv[]) /* Remove old lock */ snprintf(path, sizeof(path), "%s/.gislock", mapset_old_path); - remove(path); + if (remove(path) != 0) { + G_warning(_("Failed to remove old lock file <%s>: %s"), path, + strerror(errno)); + } G_free(mapset_old_path); diff --git a/general/g.mapsets/list.c b/general/g.mapsets/list.c index 49e10486397..cd4b1a38802 100644 --- a/general/g.mapsets/list.c +++ b/general/g.mapsets/list.c @@ -3,22 +3,22 @@ #include #include #include "local_proto.h" -#include +#include // Function to initialize a JSON object with a mapsets array -static JSON_Object *initialize_json_object(void) +static G_JSON_Object *initialize_json_object(void) { - JSON_Value *root_value = json_value_init_object(); + G_JSON_Value *root_value = G_json_value_init_object(); if (!root_value) { G_fatal_error(_("Failed to initialize JSON object. Out of memory?")); } - JSON_Object *root_object = json_value_get_object(root_value); - json_object_set_value(root_object, "mapsets", json_value_init_array()); + G_JSON_Object *root_object = G_json_value_get_object(root_value); + G_json_object_set_value(root_object, "mapsets", G_json_value_init_array()); - JSON_Array *mapsets = json_object_get_array(root_object, "mapsets"); + G_JSON_Array *mapsets = G_json_object_get_array(root_object, "mapsets"); if (!mapsets) { - json_value_free(root_value); + G_json_value_free(root_value); G_fatal_error(_("Failed to initialize mapsets array. Out of memory?")); } @@ -26,17 +26,17 @@ static JSON_Object *initialize_json_object(void) } // Function to serialize and print JSON object -static void serialize_and_print_json_object(JSON_Value *root_value) +static void serialize_and_print_json_object(G_JSON_Value *root_value) { - char *serialized_string = json_serialize_to_string_pretty(root_value); + char *serialized_string = G_json_serialize_to_string_pretty(root_value); if (!serialized_string) { - json_value_free(root_value); + G_json_value_free(root_value); G_fatal_error(_("Failed to serialize JSON to pretty format.")); } fprintf(stdout, "%s\n", serialized_string); - json_free_serialized_string(serialized_string); - json_value_free(root_value); + G_json_free_serialized_string(serialized_string); + G_json_value_free(root_value); } void list_available_mapsets(const char **mapset_name, int nmapsets, @@ -73,27 +73,27 @@ void list_accessible_mapsets(const char *fs) void list_accessible_mapsets_json(void) { const char *name; - JSON_Object *root_object = initialize_json_object(); - JSON_Array *mapsets = json_object_get_array(root_object, "mapsets"); + G_JSON_Object *root_object = initialize_json_object(); + G_JSON_Array *mapsets = G_json_object_get_array(root_object, "mapsets"); for (int n = 0; (name = G_get_mapset_name(n)); n++) { - json_array_append_string(mapsets, name); + G_json_array_append_string(mapsets, name); } serialize_and_print_json_object( - json_object_get_wrapping_value(root_object)); + G_json_object_get_wrapping_value(root_object)); } // Lists available mapsets from a provided array in JSON format void list_avaliable_mapsets_json(const char **mapset_names, int nmapsets) { - JSON_Object *root_object = initialize_json_object(); - JSON_Array *mapsets = json_object_get_array(root_object, "mapsets"); + G_JSON_Object *root_object = initialize_json_object(); + G_JSON_Array *mapsets = G_json_object_get_array(root_object, "mapsets"); for (int n = 0; n < nmapsets; n++) { - json_array_append_string(mapsets, mapset_names[n]); + G_json_array_append_string(mapsets, mapset_names[n]); } serialize_and_print_json_object( - json_object_get_wrapping_value(root_object)); + G_json_object_get_wrapping_value(root_object)); } diff --git a/general/g.message/g.message.html b/general/g.message/g.message.html index 67587eaf002..8bbb06914fb 100644 --- a/general/g.message/g.message.html +++ b/general/g.message/g.message.html @@ -1,7 +1,7 @@

DESCRIPTION

g.message prints a message, warning, progress info, or fatal error -in the GRASS GIS way. +in the GRASS way. This program is to be used in Shell/Perl/Python scripts, so the author does not need to use the echo program. The advantage of g.message is diff --git a/general/g.message/g.message.md b/general/g.message/g.message.md index bac30ba28ae..854d2223cb2 100644 --- a/general/g.message/g.message.md +++ b/general/g.message/g.message.md @@ -1,7 +1,7 @@ ## DESCRIPTION *g.message* prints a message, warning, progress info, or fatal error in -the GRASS GIS way. This program is to be used in Shell/Perl/Python +the GRASS way. This program is to be used in Shell/Perl/Python scripts, so the author does not need to use the `echo` program. The advantage of *g.message* is that it formats messages just like other GRASS modules do and that its functionality is influenced by the diff --git a/general/g.parser/g.parser.html b/general/g.parser/g.parser.html index c438a675454..e6329a46517 100644 --- a/general/g.parser/g.parser.html +++ b/general/g.parser/g.parser.html @@ -455,7 +455,7 @@

Example code for SHELL

# %end if [ -z "$GISBASE" ] ; then - echo "You must be in GRASS GIS to run this program." 1>&2 + echo "You must be in GRASS to run this program." 1>&2 exit 1 fi @@ -517,7 +517,7 @@

Example code for Perl

# %end if ( !$ENV{'GISBASE'} ) { - printf(STDERR "You must be in GRASS GIS to run this program.\n"); + printf(STDERR "You must be in GRASS to run this program.\n"); exit 1; } @@ -548,8 +548,8 @@

Example code for Perl

Easy creation of a script

-By using the --script flag with any GRASS GIS module (must be run in -a GRASS GIS session) header, description, keywords, parameters, flags and +By using the --script flag with any GRASS module (must be run in +a GRASS session) header, description, keywords, parameters, flags and a template main Python script section will be printed in the terminal which can be saved to a file and used for further script programming.

@@ -677,7 +677,7 @@

SEE ALSO

Related Wiki pages: -Using GRASS GIS with other programming languages +Using GRASS with other programming languages

AUTHOR

diff --git a/general/g.parser/g.parser.md b/general/g.parser/g.parser.md index 5df31ff7e01..fab49d78b6b 100644 --- a/general/g.parser/g.parser.md +++ b/general/g.parser/g.parser.md @@ -445,7 +445,7 @@ if __name__ == "__main__": # %end if [ -z "$GISBASE" ] ; then - echo "You must be in GRASS GIS to run this program." 1>&2 + echo "You must be in GRASS to run this program." 1>&2 exit 1 fi @@ -507,7 +507,7 @@ use strict; # %end if ( !$ENV{'GISBASE'} ) { - printf(STDERR "You must be in GRASS GIS to run this program.\n"); + printf(STDERR "You must be in GRASS to run this program.\n"); exit 1; } @@ -538,8 +538,8 @@ printf ("Value of GIS_OPT_vect: '%s'\n", $ENV{'GIS_OPT_VECTOR'}); ### Easy creation of a script -By using the **--script** flag with any GRASS GIS module (must be run in -a GRASS GIS session) header, description, keywords, parameters, flags +By using the **--script** flag with any GRASS module (must be run in +a GRASS session) header, description, keywords, parameters, flags and a template main Python script section will be printed in the terminal which can be saved to a file and used for further script programming. @@ -665,7 +665,7 @@ Overview table: [Parser standard options](parser_standard_options.md) [Style Guide: Developing Python scripts](https://github.com/OSGeo/grass/blob/main/doc/development/style_guide.md#developing-python-scripts) -Related Wiki pages: [Using GRASS GIS with other programming +Related Wiki pages: [Using GRASS with other programming languages](https://grasswiki.osgeo.org/wiki/Category:Linking_to_other_languages) ## AUTHOR diff --git a/general/g.parser/test.pl b/general/g.parser/test.pl index 395ce2a6b80..db5d6730659 100755 --- a/general/g.parser/test.pl +++ b/general/g.parser/test.pl @@ -26,7 +26,7 @@ # %end if ( !$ENV{'GISBASE'} ) { - printf(STDERR "You must be in GRASS GIS to run this program.\n"); + printf(STDERR "You must be in GRASS to run this program.\n"); exit 1; } diff --git a/general/g.parser/test.sh b/general/g.parser/test.sh index a57f3c3b65f..803c2a750be 100755 --- a/general/g.parser/test.sh +++ b/general/g.parser/test.sh @@ -26,7 +26,7 @@ # %end if [ -z "$GISBASE" ] ; then - echo "You must be in GRASS GIS to run this program." 1>&2 + echo "You must be in GRASS to run this program." 1>&2 exit 1 fi 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 81c8d5ed86f..e1fae7d157f 100644 --- a/general/g.proj/output.c +++ b/general/g.proj/output.c @@ -21,7 +21,7 @@ #include #include #include -#include +#include #ifdef HAVE_OGR #include @@ -30,30 +30,34 @@ #include "local_proto.h" static int check_xy(enum OutputFormat); -static void print_json(JSON_Value *); +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; - JSON_Value *value = NULL; - 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 = json_value_init_object(); - if (value == NULL) { - G_fatal_error( - _("Failed to initialize JSON object. Out of memory?")); - } - object = 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: - 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: - 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: - 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; } @@ -342,8 +333,8 @@ void print_wkt(int esristyle, int dontprettify) static int check_xy(enum OutputFormat format) { - JSON_Value *value = NULL; - JSON_Object *object = NULL; + G_JSON_Value *value = NULL; + G_JSON_Object *object = NULL; if (cellhd.proj == PROJECTION_XY) { switch (format) { @@ -354,14 +345,15 @@ static int check_xy(enum OutputFormat format) fprintf(stdout, "XY location (unprojected)\n"); break; case JSON: - value = json_value_init_object(); + value = G_json_value_init_object(); if (value == NULL) { G_fatal_error( _("Failed to initialize JSON object. Out of memory?")); } - object = json_object(value); + object = G_json_object(value); - json_object_set_string(object, "name", "xy_location_unprojected"); + G_json_object_set_string(object, "name", + "XY location (unprojected)"); print_json(value); break; @@ -375,14 +367,71 @@ static int check_xy(enum OutputFormat format) return 0; } -void print_json(JSON_Value *value) +void print_json(G_JSON_Value *value) { - char *serialized_string = json_serialize_to_string_pretty(value); + char *serialized_string = G_json_serialize_to_string_pretty(value); if (serialized_string == NULL) { G_fatal_error(_("Failed to initialize pretty JSON string.")); } puts(serialized_string); - json_free_serialized_string(serialized_string); - json_value_free(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 19c327dc3a7..23c2947b922 100644 --- a/general/g.proj/testsuite/test_g_proj.py +++ b/general/g.proj/testsuite/test_g_proj.py @@ -19,7 +19,29 @@ class GProjTestCase(TestCase): """Test cases for g.proj module.""" # Expected projection keys for different outputs - reference = [ + # note that proj lib definition is used for all + # coordinate operations if available + + # since PROJ6, projection info is no longer consistent + # between GRASS native info and info derived directly from + # PROJ lib using SRID or WKT because GRASS native info is + # incomplete with regard to datum + + # reference for output generated with proj lib: wkt, proj4 + reference_proj = [ + "proj", + "ellps", + "lat_1", + "lat_2", + "lat_0", + "lon_0", + "x_0", + "y_0", + "no_defs", + ] + + # reference for output generated from old GRASS projinfo: shell, json + reference_grass = [ "proj", "a", "lat_1", @@ -31,13 +53,18 @@ class GProjTestCase(TestCase): "no_defs", ] - def assert_keys_in_output(self, output, prefix=""): + def assert_keys_in_proj_output(self, output, prefix=""): + """Helper method to assert that each key (optionally prefixed) exists in the given output string.""" + for key in self.reference_proj: + self.assertIn(prefix + key, output) + + def assert_keys_in_grass_output(self, output, prefix=""): """Helper method to assert that each key (optionally prefixed) exists in the given output string.""" - for key in self.reference: + for key in self.reference_grass: self.assertIn(prefix + key, output) def test_wkt_output(self): - """Test if g.proj returns WKT""" + """Test if g.proj returns WKT2, WKT1 is not accepted""" module_flag = SimpleModule("g.proj", flags="p", format="wkt") self.assertModule(module_flag) result_flag = module_flag.outputs.stdout @@ -56,19 +83,19 @@ def test_proj_info_output(self): self.assertModule(module) result = module.outputs.stdout self.assertIn("PROJ_INFO", result) - self.assert_keys_in_output(result) + self.assert_keys_in_grass_output(result) def test_proj4_output(self): """Test if g.proj returns consistent PROJ4 output.""" module_flag = SimpleModule("g.proj", flags="p", format="proj4") self.assertModule(module_flag) result_flag = module_flag.outputs.stdout - self.assert_keys_in_output(result_flag, "+") + self.assert_keys_in_proj_output(result_flag, "+") module_format = SimpleModule("g.proj", flags="p", format="proj4") self.assertModule(module_format) result_format = module_format.outputs.stdout - self.assert_keys_in_output(result_format, "+") + self.assert_keys_in_proj_output(result_format, "+") self.assertEqual(result_flag, result_format) @@ -77,21 +104,25 @@ def test_shell_output(self): module_flag = SimpleModule("g.proj", flags="p", format="shell") self.assertModule(module_flag) result_flag = module_flag.outputs.stdout - self.assert_keys_in_output(result_flag) + self.assert_keys_in_grass_output(result_flag) module_format = SimpleModule("g.proj", flags="p", format="shell") self.assertModule(module_format) result_format = module_format.outputs.stdout - self.assert_keys_in_output(result_format) + self.assert_keys_in_grass_output(result_format) 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.""" - module = SimpleModule("g.proj", flags="p", format="json") + # proj has its own PROJJSON format, use this? + module = SimpleModule("g.proj", flags="p", format="projjson") self.assertModule(module) result = json.loads(module.outputs.stdout) - self.assert_keys_in_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/general/g.region/g.region.html b/general/g.region/g.region.html index 2eb4e58aa08..d00832cbdca 100644 --- a/general/g.region/g.region.html +++ b/general/g.region/g.region.html @@ -125,7 +125,7 @@

NOTES

This can be used for example to print modified region values for further use without actually modifying the current region. Similarly, -o flag forces to update current region file even when e.g., only -printing was specified. Flag -o was added in GRASS GIS version 8 to simulate +printing was specified. Flag -o was added in GRASS version 8 to simulate g.region behavior in prior versions when current region file was always updated unless -u was specified. diff --git a/general/g.region/local_proto.h b/general/g.region/local_proto.h index 31c1bd21e65..354d773cbe2 100644 --- a/general/g.region/local_proto.h +++ b/general/g.region/local_proto.h @@ -13,7 +13,7 @@ #define PRINT_GMT 0x200 #define PRINT_WMS 0x400 -#include +#include enum OutputFormat { PLAIN, SHELL, JSON }; @@ -22,6 +22,6 @@ int zoom(struct Cell_head *, const char *, const char *); /* printwindow.c */ void print_window(struct Cell_head *, int, int, enum OutputFormat, - JSON_Object *); + G_JSON_Object *); #endif diff --git a/general/g.region/main.c b/general/g.region/main.c index 5a9abfc1414..3867b61320f 100644 --- a/general/g.region/main.c +++ b/general/g.region/main.c @@ -21,7 +21,7 @@ #include #include #include -#include +#include #include #include "local_proto.h" @@ -44,8 +44,8 @@ int main(int argc, char *argv[]) int pix; bool update_file = false; enum OutputFormat format; - JSON_Value *root_value; - JSON_Object *root_object; + G_JSON_Value *root_value; + G_JSON_Object *root_object; struct GModule *module; struct { @@ -465,12 +465,12 @@ int main(int argc, char *argv[]) if (strcmp(parm.format->answer, "json") == 0) { format = JSON; - root_value = json_value_init_object(); + root_value = G_json_value_init_object(); if (root_value == NULL) { G_fatal_error( _("Failed to initialize JSON object. Out of memory?")); } - root_object = json_object(root_value); + root_object = G_json_object(root_value); } else if (strcmp(parm.format->answer, "shell") == 0 || (print_flag & PRINT_SH)) { @@ -504,7 +504,9 @@ int main(int argc, char *argv[]) for (; *rast_ptr != NULL; rast_ptr++) { char rast_name[GNAME_MAX]; - strcpy(rast_name, *rast_ptr); + if (G_strlcpy(rast_name, *rast_ptr, sizeof(rast_name)) >= + sizeof(rast_name)) + G_fatal_error(_("Raster map name too long: <%s>"), *rast_ptr); mapset = G_find_raster2(rast_name, ""); if (!mapset) G_fatal_error(_("Raster map <%s> not found"), rast_name); @@ -559,7 +561,9 @@ int main(int argc, char *argv[]) char vect_name[GNAME_MAX]; struct Cell_head map_window; - strcpy(vect_name, *vect_ptr); + if (G_strlcpy(vect_name, *vect_ptr, sizeof(vect_name)) >= + sizeof(vect_name)) + G_fatal_error(_("Vector map name too long: <%s>"), *vect_ptr); mapset = G_find_vector2(vect_name, ""); if (!mapset) G_fatal_error(_("Vector map <%s> not found"), vect_name); @@ -981,13 +985,13 @@ int main(int argc, char *argv[]) if (format == JSON) { char *serialized_string = NULL; - serialized_string = json_serialize_to_string_pretty(root_value); + serialized_string = G_json_serialize_to_string_pretty(root_value); if (serialized_string == NULL) { G_fatal_error(_("Failed to initialize pretty JSON string.")); } puts(serialized_string); - json_free_serialized_string(serialized_string); - json_value_free(root_value); + G_json_free_serialized_string(serialized_string); + G_json_value_free(root_value); } exit(EXIT_SUCCESS); diff --git a/general/g.region/printwindow.c b/general/g.region/printwindow.c index b6cfcf63bd5..d48b7fa33c6 100644 --- a/general/g.region/printwindow.c +++ b/general/g.region/printwindow.c @@ -22,7 +22,7 @@ static double get_shift(double east) } void print_window(struct Cell_head *window, int print_flag, int flat_flag, - enum OutputFormat format, JSON_Object *root_object) + enum OutputFormat format, G_JSON_Object *root_object) { const char *prj, *datum, *ellps; int x, width = 11; @@ -109,7 +109,7 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag, /* flag.print & flag.gprint */ if (print_flag & PRINT_REG) { prj = G_database_projection_name(); - if (!prj) + if (!prj && format != JSON) prj = "** unknown **"; switch (format) { @@ -123,6 +123,7 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag, fprintf(stdout, "%-*s %d\n", width, "zone:", window->zone); break; case JSON: + // We print CRS info at the end of the JSON output, not here. break; } @@ -258,41 +259,71 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag, #endif break; case JSON: - json_object_set_number(root_object, "north", window->north); - json_object_set_number(root_object, "south", window->south); - json_object_set_number(root_object, "west", window->west); - json_object_set_number(root_object, "east", window->east); - json_object_set_number(root_object, "nsres", d_nsres); - json_object_set_number(root_object, "ewres", d_ewres); - json_object_set_number(root_object, "rows", window->rows); - json_object_set_number(root_object, "cols", window->cols); + G_json_object_set_number(root_object, "north", window->north); + G_json_object_set_number(root_object, "south", window->south); + G_json_object_set_number(root_object, "west", window->west); + G_json_object_set_number(root_object, "east", window->east); + G_json_object_set_number(root_object, "nsres", d_nsres); + G_json_object_set_number(root_object, "ewres", d_ewres); + G_json_object_set_number(root_object, "rows", window->rows); + G_json_object_set_number(root_object, "cols", window->cols); if (print_flag & PRINT_3D) { - json_object_set_number(root_object, "nsres3", d_nsres3); - json_object_set_number(root_object, "ewres3", d_ewres3); - json_object_set_number(root_object, "top", window->top); - json_object_set_number(root_object, "bottom", window->bottom); - json_object_set_number(root_object, "rows3", window->rows3); - json_object_set_number(root_object, "cols3", window->cols3); - json_object_set_number(root_object, "tbres", d_tbres); - json_object_set_number(root_object, "depths", window->depths); + G_json_object_set_number(root_object, "nsres3", d_nsres3); + G_json_object_set_number(root_object, "ewres3", d_ewres3); + G_json_object_set_number(root_object, "top", window->top); + G_json_object_set_number(root_object, "bottom", window->bottom); + G_json_object_set_number(root_object, "rows3", window->rows3); + G_json_object_set_number(root_object, "cols3", window->cols3); + G_json_object_set_number(root_object, "tbres", d_tbres); + G_json_object_set_number(root_object, "depths", window->depths); } #ifdef HAVE_LONG_LONG_INT - json_object_set_number(root_object, "cells", - (long long)window->rows * window->cols); + G_json_object_set_number(root_object, "cells", + (long long)window->rows * window->cols); if (print_flag & PRINT_3D) - json_object_set_number(root_object, "cells3", - (long long)window->rows3 * - window->cols3 * window->depths); + G_json_object_set_number(root_object, "cells3", + (long long)window->rows3 * + window->cols3 * window->depths); #else - json_object_set_number(root_object, "cells", - (long)window->rows * window->cols); + G_json_object_set_number(root_object, "cells", + (long)window->rows * window->cols); if (print_flag & PRINT_3D) - json_object_set_number(root_object, "cells3", - (long)window->rows3 * window->cols3 * - window->depths); + G_json_object_set_number(root_object, "cells3", + (long)window->rows3 * window->cols3 * + window->depths); #endif + // The library functions try hard to provide a name and the creation + // process actually stores 'unknown' if the name is not available, + // so something is always read. Here, we assume that single word + // 'unknown' is the same as no name. + if (prj && strcmp(prj, "unknown") != 0) + G_json_object_dotset_string(root_object, "crs.name", prj); + else + G_json_object_dotset_null(root_object, "crs.name"); + const char *type_string; + if (window->proj == 0) + type_string = "xy"; + else if (window->proj == 1) + type_string = "utm"; + else if (window->proj == 3) + type_string = "ll"; + else + // This is including code 2 (state plane) which still exists + // in the library define, but should not occur, so we don't + // handle it here (possibly creating output: 2 | other). + type_string = "other"; + G_json_object_dotset_string(root_object, "crs.type", type_string); + G_json_object_dotset_number(root_object, "crs.type_code", + window->proj); + // If the zone is 0 for other than UTM, we consider that as zone not + // set, but we still allow non-zero for non-UTM (if ever set). + if (window->proj != 1 && window->zone == 0) + G_json_object_dotset_null(root_object, "crs.zone"); + else + G_json_object_dotset_number(root_object, "crs.zone", + window->zone); break; } } @@ -444,16 +475,16 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag, fprintf(stdout, "%-*s %11s\n", width, "center latitude:", buf); break; case JSON: - json_object_set_number(root_object, "nw_long", lo1); - json_object_set_number(root_object, "nw_lat", la1); - json_object_set_number(root_object, "ne_long", lo2); - json_object_set_number(root_object, "ne_lat", la2); - json_object_set_number(root_object, "se_long", lo3); - json_object_set_number(root_object, "se_lat", la3); - json_object_set_number(root_object, "sw_long", lo4); - json_object_set_number(root_object, "sw_lat", la4); - json_object_set_number(root_object, "center_long", loc); - json_object_set_number(root_object, "center_lat", lac); + G_json_object_set_number(root_object, "nw_long", lo1); + G_json_object_set_number(root_object, "nw_lat", la1); + G_json_object_set_number(root_object, "ne_long", lo2); + G_json_object_set_number(root_object, "ne_lat", la2); + G_json_object_set_number(root_object, "se_long", lo3); + G_json_object_set_number(root_object, "se_lat", la3); + G_json_object_set_number(root_object, "sw_long", lo4); + G_json_object_set_number(root_object, "sw_lat", la4); + G_json_object_set_number(root_object, "center_long", loc); + G_json_object_set_number(root_object, "center_lat", lac); break; } @@ -468,8 +499,8 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag, fprintf(stdout, "%-*s %d\n", width, "cols:", window->cols); break; case JSON: - json_object_set_number(root_object, "rows", window->rows); - json_object_set_number(root_object, "cols", window->cols); + G_json_object_set_number(root_object, "rows", window->rows); + G_json_object_set_number(root_object, "cols", window->cols); break; } } @@ -511,10 +542,10 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag, } break; case JSON: - json_object_set_number(root_object, "ns_extent", - window->north - window->south); - json_object_set_number(root_object, "ew_extent", - window->east - window->west); + G_json_object_set_number(root_object, "ns_extent", + window->north - window->south); + G_json_object_set_number(root_object, "ew_extent", + window->east - window->west); break; } } @@ -545,10 +576,10 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag, } break; case JSON: - json_object_set_number(root_object, "center_easting", - (window->west + window->east) / 2.); - json_object_set_number(root_object, "center_northing", - (window->north + window->south) / 2.); + G_json_object_set_number(root_object, "center_easting", + (window->west + window->east) / 2.); + G_json_object_set_number(root_object, "center_northing", + (window->north + window->south) / 2.); break; } } @@ -559,7 +590,7 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag, switch (format) { case JSON: snprintf(gmt, 120, "%s/%s/%s/%s", west, east, south, north); - json_object_set_string(root_object, "GMT", gmt); + G_json_object_set_string(root_object, "GMT", gmt); break; default: fprintf(stdout, "%s/%s/%s/%s\n", west, east, south, north); @@ -573,7 +604,7 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag, switch (format) { case JSON: snprintf(wms, 150, "bbox=%s,%s,%s,%s", west, south, east, north); - json_object_set_string(root_object, "WMS", wms); + G_json_object_set_string(root_object, "WMS", wms); break; default: G_format_northing(window->north, north, -1); @@ -667,7 +698,8 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag, "convergence angle:", convergence); break; case JSON: - json_object_set_number(root_object, "converge_angle", convergence); + G_json_object_set_number(root_object, "converge_angle", + convergence); } } @@ -873,14 +905,14 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag, fprintf(stdout, "%-*s %s\n", width, "center latitude:", buf); break; case JSON: - json_object_set_number(root_object, "ll_n", sh_ll_n); - json_object_set_number(root_object, "ll_s", sh_ll_s); - json_object_set_number(root_object, "ll_w", sh_ll_w); - json_object_set_number(root_object, "ll_e", sh_ll_e); + G_json_object_set_number(root_object, "ll_n", sh_ll_n); + G_json_object_set_number(root_object, "ll_s", sh_ll_s); + G_json_object_set_number(root_object, "ll_w", sh_ll_w); + G_json_object_set_number(root_object, "ll_e", sh_ll_e); /* center of the largest bounding box */ - json_object_set_number(root_object, "ll_clon", loc); - json_object_set_number(root_object, "ll_clat", - (sh_ll_n + sh_ll_s) / 2.); + G_json_object_set_number(root_object, "ll_clon", loc); + G_json_object_set_number(root_object, "ll_clat", + (sh_ll_n + sh_ll_s) / 2.); } /*It should be calculated which number of rows and cols we have in diff --git a/general/g.region/tests/g_region_crs_test.py b/general/g.region/tests/g_region_crs_test.py new file mode 100644 index 00000000000..743c274ecf0 --- /dev/null +++ b/general/g.region/tests/g_region_crs_test.py @@ -0,0 +1,107 @@ +import os + +import grass.script as gs +from grass.tools import Tools + + +def test_crs_xy(tmp_path): + """Check general XY project""" + project = tmp_path / "test" + gs.create_project(project) + with ( + gs.setup.init(project, env=os.environ.copy()) as session, + Tools(session=session) as tools, + ): + data = tools.g_region(flags="p", format="json") + assert data["crs"]["type_code"] == 0 + assert data["crs"]["type"] == "xy" + # We test the value we are returning (but the value is not part of the API). + assert data["crs"]["name"] == "x,y" + assert data["crs"]["zone"] is None + + +def test_crs_utm_north(tmp_path): + """Check UTM 17N project""" + project = tmp_path / "test" + gs.create_project(project, epsg="6346") + with ( + gs.setup.init(project, env=os.environ.copy()) as session, + Tools(session=session) as tools, + ): + data = tools.g_region(flags="p", format="json") + assert data["crs"]["type_code"] == 1 + assert data["crs"]["type"] == "utm" + # We simply test the value we are returning now, but more detailed name would be nice. + assert data["crs"]["name"] == "UTM" + assert data["crs"]["zone"] == 17 + + +def test_crs_utm_south(tmp_path): + """Check UTM 17S project""" + project = tmp_path / "test" + gs.create_project(project, epsg="32717") + with ( + gs.setup.init(project, env=os.environ.copy()) as session, + Tools(session=session) as tools, + ): + data = tools.g_region(flags="p", format="json") + assert data["crs"]["type_code"] == 1 + assert data["crs"]["type"] == "utm" + # We simply test the value we are returning now, but more detailed name would be nice. + assert data["crs"]["name"] == "UTM" + assert data["crs"]["zone"] == -17 + + +def test_crs_ll(tmp_path): + """Check latitude/longitude (WGS84) project""" + project = tmp_path / "test" + gs.create_project(project, epsg="4326") + with ( + gs.setup.init(project, env=os.environ.copy()) as session, + Tools(session=session) as tools, + ): + data = tools.g_region(flags="p", format="json") + assert data["crs"]["type_code"] == 3 + assert data["crs"]["type"] == "ll" + # We test the value we are returning (but the value is not part of the API). + assert data["crs"]["name"] == "Latitude-Longitude" + assert data["crs"]["zone"] is None + + +def test_crs_other_epsg(tmp_path): + """Check other CRS (NC SPM) project from EPSG + + Note: In the past, projection code 2 was used for US state plane CRS in feet, + but we are not supporting or testing this. + """ + project = tmp_path / "test" + gs.create_project(project, epsg="3358") + with ( + gs.setup.init(project, env=os.environ.copy()) as session, + Tools(session=session) as tools, + ): + data = tools.g_region(flags="p", format="json") + assert data["crs"]["type_code"] == 99 + assert data["crs"]["type"] == "other" + # We loosely test for the name associated with the EPSG code. + # It may as well be, e.g., Lambert Conformal Conic. + # (But we should not get 'other or unknown projection' type of text.) + name = data["crs"]["name"] + assert "North Carolina".lower() in name.lower() + assert "NAD83".lower() in name.lower() + assert data["crs"]["zone"] is None + + +def test_crs_other_custom(tmp_path): + """Check a custom CRS project""" + project = tmp_path / "test" + gs.create_project(project, proj4="+proj=merc +lat_ts=56.5 +ellps=GRS80") + with ( + gs.setup.init(project, env=os.environ.copy()) as session, + Tools(session=session) as tools, + ): + data = tools.g_region(flags="p", format="json") + assert data["crs"]["type_code"] == 99 + assert data["crs"]["type"] == "other" + assert data["crs"]["name"] is None + assert data["crs"]["zone"] is None diff --git a/general/g.region/testsuite/test_g_region.py b/general/g.region/testsuite/test_g_region.py index b00c61803d4..b383c9de345 100644 --- a/general/g.region/testsuite/test_g_region.py +++ b/general/g.region/testsuite/test_g_region.py @@ -189,6 +189,12 @@ def test_format_json(self): "rows": 620, "cols": 1630, "cells": 1010600, + "crs": { + "name": "Lambert Conformal Conic", + "type": "other", + "type_code": 99, + "zone": None, + }, } output = call_module("g.region", flags="p", format="json") @@ -240,6 +246,12 @@ def test_json_with_flags(self): "ll_e": -75.36388301356145, "ll_clon": -79.91588285974797, "ll_clat": 35.17852919352317, + "crs": { + "name": "Lambert Conformal Conic", + "type": "other", + "type_code": 99, + "zone": None, + }, } output = call_module("g.region", flags="plectwmn3b", format="json") diff --git a/general/g.version/g.version.html b/general/g.version/g.version.html index abb3867c9d5..b83931cf5f8 100644 --- a/general/g.version/g.version.html +++ b/general/g.version/g.version.html @@ -1,17 +1,17 @@

DESCRIPTION

g.version prints to standard output the GRASS version number, -date, the GRASS GIS copyright (-c flag), and GRASS build information +date, the GRASS copyright (-c flag), and GRASS build information (-b flag).

NOTES

This program requires no command line arguments; the user simply types g.version on the command line to see the version number and -date of the GRASS GIS software currently being run by the user. +date of the GRASS software currently being run by the user.

-Information about GRASS GIS +Information about GRASS core GIS Library can be printed by -r flag. @@ -70,16 +70,16 @@

Full info in shell script style

Note: if revision=exported is reported instead of the git hash then the -git program was not available during compilation of GRASS GIS and the +git program was not available during compilation of GRASS and the source code did not contain the .git/ subdirectory (requires e.g. to -git clone the GRASS GIS software repository.) +git clone the GRASS software repository.) -

Citing GRASS GIS

+

Citing GRASS

The GRASS Development Team has invested significant time and effort -in creating GRASS GIS, please cite it when using it for data analysis. +in creating GRASS, please cite it when using it for data analysis. -The GRASS GIS Web site +The GRASS Web site offers citations in different styles. diff --git a/general/g.version/g.version.md b/general/g.version/g.version.md index c87ea20190c..ab87a2fd1b5 100644 --- a/general/g.version/g.version.md +++ b/general/g.version/g.version.md @@ -1,16 +1,16 @@ ## DESCRIPTION *g.version* prints to standard output the GRASS version number, date, -the GRASS GIS copyright (**-c** flag), and GRASS build information +the GRASS copyright (**-c** flag), and GRASS build information (**-b** flag). ## NOTES This program requires no command line arguments; the user simply types *g.version* on the command line to see the version number and date of -the GRASS GIS software currently being run by the user. +the GRASS software currently being run by the user. -Information about GRASS GIS core [GIS +Information about GRASS core [GIS Library](https://grass.osgeo.org/programming8/gislib.html) can be printed by **-r** flag. @@ -115,16 +115,16 @@ libgis date: 2024-04-27T09:38:49+00:00 Note: if `revision=exported` is reported instead of the git hash then -the `git` program was not available during compilation of GRASS GIS and +the `git` program was not available during compilation of GRASS and the source code did not contain the `.git/` subdirectory (requires e.g. -to `git clone` the GRASS GIS [software +to `git clone` the GRASS [software repository](https://github.com/OSGeo/grass/).) -## Citing GRASS GIS +## Citing GRASS The GRASS Development Team has invested significant time and effort in -creating GRASS GIS, please cite it when using it for data analysis. The -GRASS GIS [Web site](https://grass.osgeo.org/about/license/) offers +creating GRASS, please cite it when using it for data analysis. The +GRASS [Web site](https://grass.osgeo.org/about/license/) offers citations in different styles. ## AUTHORS diff --git a/general/g.version/main.c b/general/g.version/main.c index 338c9711cf6..3d6b3f2fa2d 100644 --- a/general/g.version/main.c +++ b/general/g.version/main.c @@ -65,8 +65,8 @@ int main(int argc, char *argv[]) struct Flag *copyright, *build, *gish_rev, *cite_flag, *shell, *extended; struct Option *fopt; enum OutputFormat format; - JSON_Value *root_value = NULL; - JSON_Object *root_object = NULL; + G_JSON_Value *root_value = NULL; + G_JSON_Object *root_object = NULL; G_gisinit(argv[0]); @@ -77,7 +77,7 @@ int main(int argc, char *argv[]) G_add_keyword(_("copyright")); G_add_keyword(_("version")); G_add_keyword(_("license")); - module->label = _("Displays GRASS GIS version info."); + module->label = _("Displays GRASS version info."); module->description = _("Optionally also prints build or copyright information."); @@ -257,8 +257,7 @@ int main(int argc, char *argv[]) case SHELL: fprintf(stdout, "libgis_revision=\n"); fprintf(stdout, "libgis_date=\n"); - G_warning( - "GRASS GIS libgis version and date number not available"); + G_warning("GRASS libgis version and date number not available"); /* this can be alternatively fatal error or it can cause fatal error later */ break; @@ -272,13 +271,12 @@ int main(int argc, char *argv[]) case JSON: G_json_object_set_null(root_object, "libgis_revision"); G_json_object_set_null(root_object, "libgis_date"); - G_warning( - "GRASS GIS libgis version and date number not available"); + G_warning("GRASS libgis version and date number not available"); break; } } if (no_libgis) { - G_debug(1, _("GRASS GIS libgis version and date number don't have " + G_debug(1, _("GRASS libgis version and date number don't have " "the expected format." " Trying to print the original strings...")); G_debug(1, _("GIS_H_VERSION=\"%s\""), GIS_H_VERSION); diff --git a/grasslib.dox b/grasslib.dox index 030044af744..deb1c07abb6 100644 --- a/grasslib.dox +++ b/grasslib.dox @@ -1,10 +1,10 @@ -/*! \mainpage GRASS GIS 8 Programmer's Manual +/*! \mainpage GRASS 8 Programmer's Manual -GRASS GIS (Geographic +GRASS (Geographic Resources Analysis Support System) is an open source, free software Geographical Information System (GIS) with raster, topological %vector, image processing, and graphics production @@ -41,7 +41,7 @@ href="https://grass.osgeo.org">https://grass.osgeo.org original: doc/grass_arch.odg --> -\image html "grass_arch.png" "GRASS GIS Architecture" +\image html "grass_arch.png" "GRASS Architecture" \section libsOverview Libraries @@ -113,7 +113,7 @@ href="https://grass.osgeo.org">https://grass.osgeo.org \subsection pythonlibs Python API - - python: See GRASS GIS Python library (https://grass.osgeo.org/grass-devel/manuals/libpython/) + - python: See GRASS Python library (https://grass.osgeo.org/grass-devel/manuals/libpython/) \subsection projlibs Projection Libraries diff --git a/gui/icons/grass.appdata.xml b/gui/icons/grass.appdata.xml index 06c0ad72142..97e9f474dd5 100644 --- a/gui/icons/grass.appdata.xml +++ b/gui/icons/grass.appdata.xml @@ -2,32 +2,32 @@ org.osgeo.grass.desktop - GRASS GIS + GRASS CC0-1.0 GPL-2.0+ grass - GRASS GIS, the Open Source Geographic Information System + GRASS, the Open Source Geographic Information System

The Geographic Resources Analysis Support System, commonly - referred to as GRASS GIS, is an Open Source + referred to as GRASS, is an Open Source Geographic Information System providing powerful raster, vector and geospatial processing capabilities in a single integrated software suite.

- GRASS GIS includes tools for spatial modeling, visualization of + GRASS includes tools for spatial modeling, visualization of raster and vector data, management and analysis of geospatial data, and the processing of satellite and aerial imagery. It also provides the capability to produce sophisticated presentation graphics and hardcopy maps.

- GRASS GIS has been translated into about twenty languages and + GRASS has been translated into about twenty languages and supports a huge array of data formats. It can be used either as a stand-alone application or as backend for other software packages such as QGIS and R geostatistics. It is distributed freely under - the terms of the GNU General Public License (GPL). GRASS GIS is a + the terms of the GNU General Public License (GPL). GRASS is a founding member of the Open Source Geospatial Foundation (OSGeo).

diff --git a/gui/icons/grass/location-create.png b/gui/icons/grass/location-create.png new file mode 100644 index 00000000000..e1566a13d92 Binary files /dev/null and b/gui/icons/grass/location-create.png differ diff --git a/gui/icons/grass/location-create.svg b/gui/icons/grass/location-create.svg new file mode 100644 index 00000000000..a7207f3a6d2 --- /dev/null +++ b/gui/icons/grass/location-create.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/wxpython/README.md b/gui/wxpython/README.md index cf73dad8c66..2fc4d78d5ee 100644 --- a/gui/wxpython/README.md +++ b/gui/wxpython/README.md @@ -1,4 +1,4 @@ -# GRASS GIS - wxPython Graphical User Interface (wxGUI) +# GRASS - wxPython Graphical User Interface (wxGUI) ## 1 - REQUIREMENTS diff --git a/gui/wxpython/animation/controller.py b/gui/wxpython/animation/controller.py index 6cf3269eeb1..f2f69334f83 100644 --- a/gui/wxpython/animation/controller.py +++ b/gui/wxpython/animation/controller.py @@ -60,7 +60,7 @@ def __init__( self.bitmapPool = bitmapPool self.mapFilesPool = mapFilesPool self.bitmapProvider = provider - for anim, win in zip(self.animations, self.mapwindows): + for anim, win in zip(self.animations, self.mapwindows, strict=False): anim.SetCallbackUpdateFrame( lambda index, dataId, win=win: self.UpdateFrame(index, win, dataId) ) @@ -527,7 +527,9 @@ def _export(self, exportInfo, decorations): animWinIndex = [] legends = [anim.legendCmd for anim in self.animationData] # determine position and sizes of bitmaps - for i, (win, anim) in enumerate(zip(self.mapwindows, self.animations)): + for i, (win, anim) in enumerate( + zip(self.mapwindows, self.animations, strict=False) + ): if anim.IsActive(): pos = win.GetPosition() animWinPos.append(pos) diff --git a/gui/wxpython/animation/data.py b/gui/wxpython/animation/data.py index a627ce00865..0d828a8b3c7 100644 --- a/gui/wxpython/animation/data.py +++ b/gui/wxpython/animation/data.py @@ -16,8 +16,8 @@ @author Anna Petrasova """ -import os import copy +from pathlib import Path from grass.script.utils import parse_key_val from grass.script import core as gcore @@ -134,7 +134,7 @@ def SetWorkspaceFile(self, fileName): if fileName == "": raise ValueError(_("No workspace file selected.")) - if not os.path.exists(fileName): + if not Path(fileName).exists(): raise OSError(_("File %s not found") % fileName) self._workspaceFile = fileName @@ -259,7 +259,7 @@ def _computeRegions(self, count, startRegion, endRegion=None, zoomValue=None): values = interpolate( startRegionDict[key], endRegionDict[key], self._mapCount ) - for value, region in zip(values, regions): + for value, region in zip(values, regions, strict=False): region[key] = value elif zoomValue: diff --git a/gui/wxpython/animation/dialogs.py b/gui/wxpython/animation/dialogs.py index 1ee8d01c174..483e328506e 100644 --- a/gui/wxpython/animation/dialogs.py +++ b/gui/wxpython/animation/dialogs.py @@ -248,7 +248,7 @@ def _fillUnitChoice(self, choiceWidget): _("second"), ] timeUnits = ["years", "months", "days", "hours", "minutes", "seconds"] - for item, cdata in zip(timeUnitsChoice, timeUnits): + for item, cdata in zip(timeUnitsChoice, timeUnits, strict=True): choiceWidget.Append(item, cdata) if self.temporalMode == TemporalMode.TEMPORAL: @@ -1017,7 +1017,7 @@ def _createDecorationsList(self, panel): buttonNames = ["time", "image", "text"] buttonLabels = [_("Add time stamp"), _("Add image"), _("Add text")] i = 0 - for buttonName, buttonLabel in zip(buttonNames, buttonLabels): + for buttonName, buttonLabel in zip(buttonNames, buttonLabels, strict=True): if buttonName == "time" and self.temporal == TemporalMode.NONTEMPORAL: continue btn = Button(panel, id=wx.ID_ANY, name=buttonName, label=buttonLabel) @@ -1443,7 +1443,7 @@ def OnRemove(self, event): def OnExport(self, event): for decor in self.decorations: if decor["name"] == "image": - if not os.path.exists(decor["file"]): + if not Path(decor["file"]).exists(): if decor["file"]: GError( parent=self, message=_("File %s not found.") % decor["file"] @@ -1456,7 +1456,7 @@ def OnExport(self, event): if self.formatChoice.GetSelection() == 0: name = self.dirBrowse.GetValue() - if not os.path.exists(name): + if not Path(name).exists(): if name: GError(parent=self, message=_("Directory %s not found.") % name) else: @@ -1553,7 +1553,7 @@ def _export_file_validation(self, filebrowsebtn, file_path, file_postfix): file_path += file_postfix base_dir = os.path.dirname(file_path) - if not os.path.exists(base_dir): + if not Path(base_dir).exists(): GError( parent=self, message=file_path_does_not_exist_err_message.format( @@ -1562,7 +1562,7 @@ def _export_file_validation(self, filebrowsebtn, file_path, file_postfix): ) return False - if os.path.exists(file_path): + if Path(file_path).exists(): overwrite_dlg = wx.MessageDialog( self.GetParent(), message=_( @@ -1660,7 +1660,7 @@ def __init__( self.tselect = Select(parent=self, type="strds") iconTheme = UserSettings.Get(group="appearance", key="iconTheme", subkey="type") bitmapPath = os.path.join(globalvar.ICONDIR, iconTheme, "layer-open.png") - if os.path.isfile(bitmapPath) and os.path.getsize(bitmapPath): + if Path(bitmapPath).is_file() and Path(bitmapPath).stat().st_size: bitmap = wx.Bitmap(name=bitmapPath) else: bitmap = wx.ArtProvider.GetBitmap( diff --git a/gui/wxpython/animation/g.gui.animation.py b/gui/wxpython/animation/g.gui.animation.py index 911c6c472dd..64f91ef8ad7 100644 --- a/gui/wxpython/animation/g.gui.animation.py +++ b/gui/wxpython/animation/g.gui.animation.py @@ -134,7 +134,7 @@ def main(): frame = AnimationFrame( parent=None, giface=StandaloneGrassInterface(), - title=_("Animation Tool - GRASS GIS"), + title=_("Animation Tool - GRASS"), ) frame.CentreOnScreen() frame.Show() diff --git a/gui/wxpython/animation/nviztask.py b/gui/wxpython/animation/nviztask.py index cd3f63ce86c..b9e209d5855 100644 --- a/gui/wxpython/animation/nviztask.py +++ b/gui/wxpython/animation/nviztask.py @@ -103,7 +103,7 @@ def _processSurface(self, surface, mapName): ("shininess_map", "shininess_value"), ("transparency_map", "transparency_value"), ) - for attr, params in zip(attributes, parameters): + for attr, params in zip(attributes, parameters, strict=True): mapname = None const = None if attr in surface["attribute"]: @@ -130,7 +130,7 @@ def _processSurface(self, surface, mapName): self._setMultiTaskParam("wire_color", value) # resolution for mode1, mode2 in zip( - ("coarse", "fine"), ("resolution_coarse", "resolution_fine") + ("coarse", "fine"), ("resolution_coarse", "resolution_fine"), strict=True ): value = surface["draw"]["resolution"][mode1] self._setMultiTaskParam(mode2, value) @@ -183,7 +183,7 @@ def _processVolume(self, volume, mapName): ("isosurf_shininess_map", "isosurf_shininess_value"), ("isosurf_transparency_map", "isosurf_transparency_value"), ) - for attr, params in zip(attributes, parameters): + for attr, params in zip(attributes, parameters, strict=True): mapname = None const = None if attr in isosurface: diff --git a/gui/wxpython/animation/provider.py b/gui/wxpython/animation/provider.py index 0be21a2ed94..46cdd07842f 100644 --- a/gui/wxpython/animation/provider.py +++ b/gui/wxpython/animation/provider.py @@ -25,6 +25,7 @@ import wx import tempfile from multiprocessing import Process, Queue +from pathlib import Path from core.gcmd import GException, DecodeString from core.settings import UserSettings @@ -122,7 +123,9 @@ def _getUniqueCmds(self): """Returns list of unique commands. Takes into account the region assigned.""" unique = [] - for cmdList, region in zip(self._cmdsForComposition, self._regions): + for cmdList, region in zip( + self._cmdsForComposition, self._regions, strict=False + ): for cmd in cmdList: if region: unique.append((tuple(cmd), tuple(sorted(region.items())))) @@ -143,10 +146,14 @@ def Unload(self): """ Debug.msg(2, "BitmapProvider.Unload") if self._cmdsForComposition: - for cmd, region in zip(self._uniqueCmds, self._regionsForUniqueCmds): + for cmd, region in zip( + self._uniqueCmds, self._regionsForUniqueCmds, strict=False + ): del self._mapFilesPool[HashCmd(cmd, region)] - for cmdList, region in zip(self._cmdsForComposition, self._regions): + for cmdList, region in zip( + self._cmdsForComposition, self._regions, strict=False + ): del self._bitmapPool[HashCmds(cmdList, region)] self._uniqueCmds = [] self._cmdsForComposition = [] @@ -165,11 +172,11 @@ def _dryRender(self, uniqueCmds, regions, force): :param regions: list of regions assigned to the commands """ count = 0 - for cmd, region in zip(uniqueCmds, regions): + for cmd, region in zip(uniqueCmds, regions, strict=False): filename = GetFileFromCmd(self._tempDir, cmd, region) if ( not force - and os.path.exists(filename) + and Path(filename).exists() and self._mapFilesPool.GetSize(HashCmd(cmd, region)) == (self.imageWidth, self.imageHeight) ): @@ -191,7 +198,7 @@ def _dryCompose(self, cmdLists, regions, force): :param force: if forced rerendering """ count = 0 - for cmdList, region in zip(cmdLists, regions): + for cmdList, region in zip(cmdLists, regions, strict=False): if ( not force and HashCmds(cmdList, region) in self._bitmapPool @@ -366,13 +373,13 @@ def Render(self, cmdList, regions, regionFor3D, bgcolor, force, nprocs): cmd_list = [] filteredCmdList = [] - for cmd, region in zip(cmdList, regions): + for cmd, region in zip(cmdList, regions, strict=False): if cmd[0] == "m.nviz.image": region = None filename = GetFileFromCmd(self._tempDir, cmd, region) if ( not force - and os.path.exists(filename) + and Path(filename).exists() and self._mapFilesPool.GetSize(HashCmd(cmd, region)) == (self.imageWidth, self.imageHeight) ): @@ -491,7 +498,7 @@ def Compose(self, cmdLists, regions, opacityList, bgcolor, force, nprocs): cmd_lists = [] filteredCmdLists = [] - for cmdList, region in zip(cmdLists, regions): + for cmdList, region in zip(cmdLists, regions, strict=False): if ( not force and HashCmds(cmdList, region) in self._bitmapPool @@ -785,7 +792,7 @@ def __init__(self, tempDir): def __call__(self): import shutil - if os.path.exists(self._tempDir): + if Path(self._tempDir).exists(): try: shutil.rmtree(self._tempDir) Debug.msg(5, "CleanUp: removed directory {t}".format(t=self._tempDir)) @@ -895,9 +902,9 @@ def test(): mapFilesPool = MapFilesPool() tempDir = "/tmp/test" - if os.path.exists(tempDir): + if Path(tempDir).exists(): shutil.rmtree(tempDir) - os.mkdir(tempDir) + Path(tempDir).mkdir() # comment this line to keep the directory after program ends # cleanUp = CleanUp(tempDir) # import atexit diff --git a/gui/wxpython/animation/temporal_manager.py b/gui/wxpython/animation/temporal_manager.py index d121d59e13a..afa4e13ff72 100644 --- a/gui/wxpython/animation/temporal_manager.py +++ b/gui/wxpython/animation/temporal_manager.py @@ -202,7 +202,7 @@ def GetLabelsAndMaps(self): timestamps = sorted(list(labelListSet), key=itemgetter(0)) newMapLists = [] - for mapList, labelList in zip(mapLists, labelLists): + for mapList, labelList in zip(mapLists, labelLists, strict=False): newMapList = [None] * len(timestamps) i = 0 # compare start time @@ -438,7 +438,7 @@ def createAbsoluteInterval(): maps="prec_1,prec_2,prec_3,prec_4,prec_5,prec_6," "temp_1,temp_2,temp_3,temp_4,temp_5,temp_6", ) - for name, fname in zip((name1, name2), (n1, n2)): + for name, fname in zip((name1, name2), (n1, n2), strict=True): gs.run_command( "t.create", overwrite=True, @@ -509,7 +509,7 @@ def createRelativeInterval(): maps="prec_1,prec_2,prec_3,prec_4,prec_5,prec_6," "temp_1,temp_2,temp_3,temp_4,temp_5,temp_6", ) - for name, fname in zip((name1, name2), (n1, n2)): + for name, fname in zip((name1, name2), (n1, n2), strict=True): gs.run_command( "t.create", overwrite=True, diff --git a/gui/wxpython/animation/utils.py b/gui/wxpython/animation/utils.py index 8c04008fcdc..322c86b76e1 100644 --- a/gui/wxpython/animation/utils.py +++ b/gui/wxpython/animation/utils.py @@ -350,7 +350,7 @@ def layerListToCmdsMatrix(layerList): else: cmdsForComposition.append([layer.cmd] * count) - return list(zip(*cmdsForComposition)) + return list(zip(*cmdsForComposition, strict=False)) def sampleCmdMatrixAndCreateNames(cmdMatrix, sampledSeries, regions): diff --git a/gui/wxpython/core/gconsole.py b/gui/wxpython/core/gconsole.py index 29565695339..26a46e8b54a 100644 --- a/gui/wxpython/core/gconsole.py +++ b/gui/wxpython/core/gconsole.py @@ -31,6 +31,7 @@ import codecs import locale +from pathlib import Path import wx from wx.lib.newevent import NewEvent @@ -607,11 +608,11 @@ def load_source(modname, filename): if sys.platform == "win32": pyFile += ".py" pyPath = os.path.join(os.environ["GISBASE"], "scripts", pyFile) - if not os.path.exists(pyPath): + if not Path(pyPath).exists(): pyPath = os.path.join( os.environ["GRASS_ADDON_BASE"], "scripts", pyFile ) - if not os.path.exists(pyPath): + if not Path(pyPath).exists(): GError( parent=self._guiparent, message=_("Module <%s> not found.") % command[0], diff --git a/gui/wxpython/core/globalvar.py b/gui/wxpython/core/globalvar.py index a3c8b50de79..a50950c62cd 100644 --- a/gui/wxpython/core/globalvar.py +++ b/gui/wxpython/core/globalvar.py @@ -102,7 +102,7 @@ def CheckForWx(): except ImportError as e: print("ERROR: wxGUI requires wxPython. {}".format(e), file=sys.stderr) print( - "You can still use GRASS GIS modules in the command line or in Python.", + "You can still use GRASS modules in the command line or in Python.", file=sys.stderr, ) sys.exit(1) @@ -200,7 +200,7 @@ def UpdateGRASSAddOnCommands(eList=None): nCmd = 0 pathList = os.getenv("PATH", "").split(os.pathsep) for path in addonPath.split(os.pathsep): - if not os.path.exists(path) or not os.path.isdir(path): + if not Path(path).exists() or not Path(path).is_dir(): continue # check if addon is in the path diff --git a/gui/wxpython/core/render.py b/gui/wxpython/core/render.py index d3a4522fc94..b75b678d504 100644 --- a/gui/wxpython/core/render.py +++ b/gui/wxpython/core/render.py @@ -57,7 +57,7 @@ def get_tempfile_name(suffix, create=False): if not create: # remove empty file to have a clean state later - os.remove(name) + Path(name).unlink() return name @@ -276,7 +276,7 @@ def IsHidden(self): def IsRendered(self) -> bool: """!Check if layer was rendered (if the image file exists)""" - return bool(os.path.exists(self.mapfile)) + return bool(Path(self.mapfile).exists()) def SetType(self, ltype): """Set layer type""" @@ -428,8 +428,8 @@ def Render(self, cmd, env): if self.layer.GetType() in {"vector", "thememap"}: if not self.layer._legrow: self.layer._legrow = grass.tempfile(create=True) - if os.path.isfile(self.layer._legrow): - os.remove(self.layer._legrow) + if (p := Path(self.layer._legrow)).is_file(): + p.unlink() env_cmd["GRASS_LEGEND_FILE"] = text_to_string(self.layer._legrow) cmd_render = copy.deepcopy(cmd) @@ -531,8 +531,8 @@ def _init(self): self.layers = [] # re-render from scratch - if os.path.exists(self.Map.mapfile): - os.remove(self.Map.mapfile) + if (p := Path(self.Map.mapfile)).is_file(): + p.unlink() def _checkRenderedSizes(self, env, layers): """Check if rendering size in current env differs from size of @@ -663,7 +663,7 @@ def OnRenderDone(self, env): if layer.GetType() == "overlay": continue - if os.path.isfile(layer.mapfile): + if Path(layer.mapfile).is_file(): maps.append(layer.mapfile) masks.append(layer.maskfile) opacities.append(str(layer.opacity)) @@ -707,8 +707,8 @@ def OnRenderDone(self, env): if layer.GetType() not in {"vector", "thememap"}: continue - if os.path.isfile(layer._legrow) and not layer.hidden: - line = Path(layer._legrow).read_text() + if (p := Path(layer._legrow)).is_file() and not layer.hidden: + line = p.read_text() outfile.write(line) new_legend.append(line) @@ -1414,11 +1414,10 @@ def DeleteLayer(self, layer, overlay=False): # it as it is, although it does not really fit with the # new system (but probably works well enough) for f in glob.glob(basefile): - os.remove(f) + Path(f).unlink() if layer.GetType() in {"vector", "thememap"}: - if os.path.isfile(layer._legrow): - os.remove(layer._legrow) + Path(layer._legrow).unlink(missing_ok=True) list_.remove(layer) @@ -1534,8 +1533,8 @@ def RemoveLayer(self, name=None, id=None): for layer in self.layers: if layer.name == name: retlayer = layer - os.remove(layer.mapfile) - os.remove(layer.maskfile) + Path(layer.mapfile).unlink() + Path(layer.maskfile).unlink() self.layers.remove(layer) return retlayer # del by id diff --git a/gui/wxpython/core/settings.py b/gui/wxpython/core/settings.py index e0758a482d6..22502b1c4f9 100644 --- a/gui/wxpython/core/settings.py +++ b/gui/wxpython/core/settings.py @@ -891,9 +891,9 @@ def ReadSettingsFile(self, settings=None): if settings is None: settings = self.userSettings - if os.path.exists(self.filePath): + if Path(self.filePath).exists(): self._readFile(settings) - elif os.path.exists(self.legacyFilePath): + elif Path(self.legacyFilePath).exists(): self._readLegacyFile(settings) # set environment variables @@ -982,9 +982,9 @@ def SaveToFile(self, settings=None): settings = self.userSettings dirPath = GetSettingsPath() - if not os.path.exists(dirPath): + if not Path(dirPath).exists(): try: - os.mkdir(dirPath) + Path(dirPath).mkdir() except OSError: GError(_("Unable to create settings directory")) return None diff --git a/gui/wxpython/core/toolboxes.py b/gui/wxpython/core/toolboxes.py index beff6291f3e..fd8b1514d32 100644 --- a/gui/wxpython/core/toolboxes.py +++ b/gui/wxpython/core/toolboxes.py @@ -57,14 +57,14 @@ def GetSettingsPath(): def _getUserToolboxesFile(): userToolboxesFile = os.path.join(GetSettingsPath(), "toolboxes", "toolboxes.xml") - if not os.path.exists(userToolboxesFile): + if not Path(userToolboxesFile).exists(): userToolboxesFile = None return userToolboxesFile def _getUserMainMenuFile(): userMainMenuFile = os.path.join(GetSettingsPath(), "toolboxes", "main_menu.xml") - if not os.path.exists(userMainMenuFile): + if not Path(userMainMenuFile).exists(): userMainMenuFile = None return userMainMenuFile @@ -96,7 +96,7 @@ def toolboxesOutdated(): """Removes auto-generated menudata.xml to let gui regenerate it next time it starts.""" path = os.path.join(GetSettingsPath(), "toolboxes", "menudata.xml") - if os.path.exists(path): + if Path(path).exists(): try_remove(path) @@ -118,7 +118,7 @@ def getMenudataFile(userRootFile, newFile, fallback): distributionRootFile = os.path.join(WXGUIDIR, "xml", userRootFile) userRootFile = os.path.join(GetSettingsPath(), "toolboxes", userRootFile) - if not os.path.exists(userRootFile): + if not Path(userRootFile).exists(): userRootFile = None # always create toolboxes directory if does not exist yet @@ -133,7 +133,7 @@ def getMenudataFile(userRootFile, newFile, fallback): # when any of main_menu.xml or toolboxes.xml are changed, # generate new menudata.xml - if os.path.exists(menudataFile): + if Path(menudataFile).exists(): # remove menu file when there is no main_menu and toolboxes if not _getUserToolboxesFile() and (not userRootFile): os.remove(menudataFile) @@ -153,9 +153,9 @@ def getMenudataFile(userRootFile, newFile, fallback): ) else: # if newer files -> generate new - menudataTime = os.path.getmtime(menudataFile) + menudataTime = Path(menudataFile).stat().st_mtime if _getUserToolboxesFile(): - if os.path.getmtime(_getUserToolboxesFile()) > menudataTime: + if Path(_getUserToolboxesFile()).stat().st_mtime > menudataTime: _debug( 2, ( @@ -165,7 +165,7 @@ def getMenudataFile(userRootFile, newFile, fallback): ) generateNew = True if userRootFile: - if os.path.getmtime(userRootFile) > menudataTime: + if Path(userRootFile).stat().st_mtime > menudataTime: _debug( 2, ( @@ -217,7 +217,7 @@ def _setupToolboxes(): """Create 'toolboxes' directory if doesn't exist.""" basePath = GetSettingsPath() path = os.path.join(basePath, "toolboxes") - if not os.path.exists(basePath): + if not Path(basePath).exists(): return None if _createPath(path): @@ -227,9 +227,9 @@ def _setupToolboxes(): def _createPath(path): """Creates path (for toolboxes) if it doesn't exist'""" - if not os.path.exists(path): + if not Path(path).exists(): try: - os.mkdir(path) + Path(path).mkdir() except OSError as e: # we cannot use GError or similar because the gui doesn't start at # all @@ -331,8 +331,8 @@ def _indent(elem, level=0): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i - for _elem in elem: - _indent(_elem, level + 1) + for elem_ in elem: + _indent(elem_, level + 1) if not elem.tail or not elem.tail.strip(): elem.tail = i elif level and (not elem.tail or not elem.tail.strip()): diff --git a/gui/wxpython/core/utils.py b/gui/wxpython/core/utils.py index 3ec6f45541f..127694280c0 100644 --- a/gui/wxpython/core/utils.py +++ b/gui/wxpython/core/utils.py @@ -22,13 +22,14 @@ import re import inspect import operator +from pathlib import Path from string import digits from typing import TYPE_CHECKING from grass.script import core as grass from grass.script import task as gtask -from grass.app.runtime import get_grass_config_dir +from grass.app.runtime import get_grass_config_dir_for_version from core.gcmd import RunCommand from core.debug import Debug @@ -610,8 +611,9 @@ def GetListOfMapsets(dbase, location, selectable=False): listOfMapsets += line.split(" ") else: for mapset in glob.glob(os.path.join(dbase, location, "*")): - if os.path.isdir(mapset) and os.path.isfile( - os.path.join(dbase, location, mapset, "WIND") + if ( + Path(mapset).is_dir() + and Path(dbase, location, mapset, "WIND").is_file() ): listOfMapsets.append(os.path.basename(mapset)) @@ -798,7 +800,9 @@ def GetFormats(writableOnly=False): def GetSettingsPath(): """Get full path to the settings directory""" version_major, version_minor, _ = grass.version()["version"].split(".") - return get_grass_config_dir(version_major, version_minor, os.environ) + return get_grass_config_dir_for_version( + version_major, version_minor, env=os.environ + ) def StoreEnvVariable(key, value=None, envFile=None): @@ -824,7 +828,7 @@ def StoreEnvVariable(key, value=None, envFile=None): # read env file environ = {} lineSkipped = [] - if os.path.exists(envFile): + if Path(envFile).exists(): try: with open(envFile) as fd: for line in fd: diff --git a/gui/wxpython/core/watchdog.py b/gui/wxpython/core/watchdog.py index 76302aeea0a..4487a1bc869 100644 --- a/gui/wxpython/core/watchdog.py +++ b/gui/wxpython/core/watchdog.py @@ -176,12 +176,12 @@ def ScheduleWatchCurrentMapset(self): ) for element, directory in self._elements_dirs: path = os.path.join(mapset_path, directory) - if not os.path.exists(path): + if not Path(path).exists(): try: - os.mkdir(path) + Path(path).mkdir() except OSError: pass - if os.path.exists(path): + if Path(path).exists(): self._observer.schedule( MapWatch(self._patterns, element, self._evt_handler), path=path, diff --git a/gui/wxpython/core/ws.py b/gui/wxpython/core/ws.py index 42b1fe0e9b7..ab3079be93b 100644 --- a/gui/wxpython/core/ws.py +++ b/gui/wxpython/core/ws.py @@ -33,6 +33,8 @@ try: haveGdal = True from osgeo import gdal + + gdal.DontUseExceptions() except ImportError: haveGdal = False diff --git a/gui/wxpython/datacatalog/catalog.py b/gui/wxpython/datacatalog/catalog.py index c8b02422e1b..59c5162d8ad 100644 --- a/gui/wxpython/datacatalog/catalog.py +++ b/gui/wxpython/datacatalog/catalog.py @@ -28,6 +28,7 @@ from gui_core.wrap import Menu from gui_core.forms import GUI from core.settings import UserSettings +from core.gcmd import GError from grass.pydispatch.signal import Signal from grass.script.utils import clock @@ -38,6 +39,8 @@ get_reason_id_mapset_not_usable, is_fallback_session, is_first_time_user, + is_location_valid, + get_location_invalid_reason, ) @@ -198,34 +201,6 @@ def OnReloadCurrentMapset(self, event): """Reload current mapset tree only""" self.tree.ReloadCurrentMapset() - def OnAddGrassDB(self, event): - """Add grass database""" - dlg = wx.DirDialog( - self, - _("Choose GRASS data directory:"), - str(Path.cwd()), - wx.DD_DEFAULT_STYLE, - ) - if dlg.ShowModal() == wx.ID_OK: - grassdatabase = dlg.GetPath() - grassdb_node = self.tree.InsertGrassDb(name=grassdatabase) - - # Offer to create a new location - if grassdb_node and not any(Path(grassdatabase).iterdir()): - message = _( - "Do you want to create a new project (also known as location)?" - ) - dlg2 = wx.MessageDialog( - self, - message=message, - caption=_("Create project?"), - style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, - ) - if dlg2.ShowModal() == wx.ID_YES: - self.tree.CreateLocation(grassdb_node) - dlg2.Destroy() - dlg.Destroy() - def OnCreateMapset(self, event): """Create new mapset in current location""" db_node, loc_node, mapset_node = self.tree.GetCurrentDbLocationMapsetNode() @@ -236,6 +211,31 @@ def OnCreateLocation(self, event): db_node, loc_node, mapset_node = self.tree.GetCurrentDbLocationMapsetNode() self.tree.CreateLocation(db_node) + def OnAddProject(self, event): + dlg = wx.DirDialog( + self, + _("Choose GRASS project:"), + str(Path.cwd()), + wx.DD_DEFAULT_STYLE, + ) + if dlg.ShowModal() == wx.ID_OK: + project = Path(dlg.GetPath()) + grassdatabase = None + if is_location_valid(project): + grassdatabase = project.parent + elif is_location_valid(project.parent): + grassdatabase = project.parent.parent + else: + for child in project.iterdir(): + if is_location_valid(child): + grassdatabase = project + break + if grassdatabase: + self.tree.InsertGrassDb(name=str(grassdatabase)) + else: + error = get_location_invalid_reason(project.parent, project.name) + GError(parent=self.parent, message=error) + def OnDownloadLocation(self, event): """Download location to current grass database""" db_node, loc_node, mapset_node = self.tree.GetCurrentDbLocationMapsetNode() @@ -302,10 +302,6 @@ def GuiParseCommand(self, cmd): def OnMoreOptions(self, event): self.giface.Help(entry="topic_import") - def SetRestriction(self, restrict): - """Allow editing other mapsets or restrict editing to current mapset""" - self.tree.SetRestriction(restrict) - def Filter(self, text, element=None): self.tree.Filter(text=text, element=element) diff --git a/gui/wxpython/datacatalog/frame.py b/gui/wxpython/datacatalog/frame.py index bc125112ea6..da51364c172 100644 --- a/gui/wxpython/datacatalog/frame.py +++ b/gui/wxpython/datacatalog/frame.py @@ -39,7 +39,7 @@ def __init__(self, parent, giface=None, title=_("Data Catalog")): # buttons self.btnClose = Button(parent=self.panel, id=wx.ID_CLOSE) - self.btnClose.SetToolTip(_("Close GRASS GIS Data Catalog")) + self.btnClose.SetToolTip(_("Close GRASS Data Catalog")) self.btnClose.SetDefault() # events diff --git a/gui/wxpython/datacatalog/g.gui.datacatalog.py b/gui/wxpython/datacatalog/g.gui.datacatalog.py index d9dab11d095..44e50abd748 100644 --- a/gui/wxpython/datacatalog/g.gui.datacatalog.py +++ b/gui/wxpython/datacatalog/g.gui.datacatalog.py @@ -47,7 +47,7 @@ def main(): frame = DataCatalogFrame( parent=None, giface=StandaloneGrassInterface(), - title=_("Data Catalog - GRASS GIS"), + title=_("Data Catalog - GRASS"), ) frame.CentreOnScreen() frame.Show() diff --git a/gui/wxpython/datacatalog/infomanager.py b/gui/wxpython/datacatalog/infomanager.py index d3daf4d38fa..086211f5f8f 100644 --- a/gui/wxpython/datacatalog/infomanager.py +++ b/gui/wxpython/datacatalog/infomanager.py @@ -37,7 +37,7 @@ def ShowDataStructureInfo(self, onCreateLocationHandler): (_("Learn more"), self._onLearnMore), ] message = _( - "GRASS GIS helps you organize your data using projects (locations) " + "GRASS helps you organize your data using projects (locations) " "which contain mapsets (subprojects). All data in one project is " "in the same coordinate reference system (CRS).\n\n" "You are currently in mapset PERMANENT in default project {loc} " @@ -81,7 +81,7 @@ def ShowFallbackSessionInfo(self, reason_id): """Show info when last used mapset is not usable""" string = self._text_from_reason_id(reason_id) message = _( - "{string} GRASS GIS has started in a temporary project. " + "{string} GRASS has started in a temporary project. " "To continue, use Data Catalog below to switch to a different project." ).format( string=string, @@ -94,7 +94,7 @@ def ShowLockedMapsetInfo(self, OnSwitchMapsetHandler): buttons = [(_("Switch to last used mapset"), OnSwitchMapsetHandler)] message = _( "Last used mapset in path '{mapsetpath}' is currently in use. " - "GRASS GIS has started in a temporary project. " + "GRASS has started in a temporary project. " "To continue, use Data Catalog below to switch to a different project " "or remove lock file and switch to the last used mapset." ).format(mapsetpath=last_used_mapset_path) @@ -120,4 +120,4 @@ def _text_from_reason_id(self, reason_id): return reason def _onLearnMore(self, event): - self._giface.Help(entry="grass_database") + self._giface.Help(entry="grass_projects") diff --git a/gui/wxpython/datacatalog/toolbars.py b/gui/wxpython/datacatalog/toolbars.py index c9c8e2281a9..3111704779d 100644 --- a/gui/wxpython/datacatalog/toolbars.py +++ b/gui/wxpython/datacatalog/toolbars.py @@ -22,25 +22,20 @@ icons = { "reloadTree": MetaIcon(img="redraw", label=_("Reload GRASS projects")), "reloadMapset": MetaIcon(img="reload", label=_("Reload current GRASS mapset only")), - "unlocked": MetaIcon( - img="edit", label=_("Restrict edits to the current mapset only") - ), - "locked": MetaIcon( - img="edit", label=_("Allow edits outside of the current mapset") - ), - "addGrassDB": MetaIcon( - img="grassdb-add", label=_("Add existing or create new database") - ), "addMapset": MetaIcon( img="mapset-add", label=_("Create new mapset in current project") ), - "addLocation": MetaIcon( + "addProject": MetaIcon( img="location-add", - label=_("Create new project (location) in current GRASS database"), + label=_("Add an existing project"), + ), + "createProject": MetaIcon( + img="location-create", + label=_("Create new project"), ), "downloadLocation": MetaIcon( img="location-download", - label=_("Download sample project (location) to current GRASS database"), + label=_("Download sample project"), ), "importRaster": MetaIcon( img="raster-import", label=_("Import raster data [r.import]") @@ -126,20 +121,14 @@ def _toolbarData(self): self.parent.OnReloadCurrentMapset, ), ( - ("lock", icons["locked"].label), - icons["locked"], - self.OnSetRestriction, - wx.ITEM_CHECK, - ), - ( - ("addGrassDB", icons["addGrassDB"].label), - icons["addGrassDB"], - self.parent.OnAddGrassDB, + ("createProject", icons["createProject"].label), + icons["createProject"], + self.parent.OnCreateLocation, ), ( - ("addLocation", icons["addLocation"].label), - icons["addLocation"], - self.parent.OnCreateLocation, + ("addProject", icons["addProject"].label), + icons["addProject"], + self.parent.OnAddProject, ), ( ("downloadLocation", icons["downloadLocation"].label), @@ -168,13 +157,3 @@ def _toolbarData(self): ), ) ) - - def OnSetRestriction(self, event): - if self.GetToolState(self.lock): - self.SetToolNormalBitmap(self.lock, icons["unlocked"].GetBitmap()) - self.SetToolShortHelp(self.lock, icons["unlocked"].GetLabel()) - self.parent.SetRestriction(restrict=False) - else: - self.SetToolNormalBitmap(self.lock, icons["locked"].GetBitmap()) - self.SetToolShortHelp(self.lock, icons["locked"].GetLabel()) - self.parent.SetRestriction(restrict=True) diff --git a/gui/wxpython/datacatalog/tree.py b/gui/wxpython/datacatalog/tree.py index 76aee7e5eff..5f19af81cd7 100644 --- a/gui/wxpython/datacatalog/tree.py +++ b/gui/wxpython/datacatalog/tree.py @@ -23,6 +23,7 @@ import re import copy from multiprocessing import Process, Queue, cpu_count +from pathlib import Path import wx @@ -227,7 +228,7 @@ def __init__( super().__init__(parent=parent, model=self._model, id=wx.ID_ANY, style=style) self._giface = giface - self._restricted = True + self._restricted = False self.showNotification = Signal("Tree.showNotification") self.showImportDataInfo = Signal("Tree.showImportDataInfo") @@ -256,6 +257,7 @@ def __init__( self.UpdateCurrentDbLocationMapsetNode() self._lastWatchdogUpdate = gs.clock() self._updateMapsetWhenIdle = None + self._watchingMapsetEnabled = False # mapset watchdog self._mapset_watchdog = MapsetWatchdog( @@ -301,13 +303,7 @@ def __init__( ) self.startEdit.connect(self.OnStartEditLabel) self.endEdit.connect(self.OnEditLabel) - self.Bind( - EVT_UPDATE_MAPSET, lambda evt: self._onWatchdogMapsetReload(evt.src_path) - ) - self.Bind(wx.EVT_IDLE, self._onUpdateMapsetWhenIdle) - self.Bind( - EVT_CURRENT_MAPSET_CHANGED, lambda evt: self._updateAfterMapsetChanged() - ) + self.EnableWatchingMapset() def _resetSelectVariables(self): """Reset variables related to item selection.""" @@ -359,7 +355,7 @@ def _getValidSavedGrassDBs(self): dbs = UserSettings.Get( group="datacatalog", key="grassdbs", subkey="listAsString" ) - return [db for db in dbs.split(",") if os.path.isdir(db)] + return [db for db in dbs.split(",") if Path(db).is_dir()] def _saveGrassDBs(self): """Save current grass dbs in tree to settings""" @@ -623,6 +619,7 @@ def _onWatchdogMapsetReload(self, event_path): self._lastWatchdogUpdate = time if (time_diff) < 0.5: self._updateMapsetWhenIdle = event_path + self.DisableWatchingMapset() return mapset_path = os.path.dirname(os.path.dirname(os.path.abspath(event_path))) location_path = os.path.dirname(os.path.abspath(mapset_path)) @@ -642,9 +639,31 @@ def UpdateAfterMapsetChanged(self): If watchdog is active, updating is skipped here to avoid double updating. """ - if not watchdog_used: + if not (watchdog_used or self._watchingMapsetEnabled): self._updateAfterMapsetChanged() + def DisableWatchingMapset(self): + """Disable watching of current mapset folder for changes with watchdog.""" + if not self._watchingMapsetEnabled: + return + self._watchingMapsetEnabled = False + self.Unbind(EVT_UPDATE_MAPSET) + self.Unbind(wx.EVT_IDLE) + self.Unbind(EVT_CURRENT_MAPSET_CHANGED) + + def EnableWatchingMapset(self): + """Enable watching of current mapset folder for changes with watchdog.""" + if self._watchingMapsetEnabled: + return + self._watchingMapsetEnabled = True + self.Bind( + EVT_UPDATE_MAPSET, lambda evt: self._onWatchdogMapsetReload(evt.src_path) + ) + self.Bind(wx.EVT_IDLE, self._onUpdateMapsetWhenIdle) + self.Bind( + EVT_CURRENT_MAPSET_CHANGED, lambda evt: self._updateAfterMapsetChanged() + ) + def GetDbNode(self, grassdb, location=None, mapset=None, map=None, map_type=None): """Returns node representing db/location/mapset/map or None if not found.""" grassdb_nodes = self._model.SearchNodes(name=grassdb, type="grassdb") @@ -1480,15 +1499,19 @@ def InsertMapset(self, name, location_node): return mapset_node def InsertLocation(self, name, grassdb_node): - """Insert new location into model and refresh tree""" - location_node = self._model.AppendNode( - parent=grassdb_node, data={"type": "location", "name": name} - ) - # reload new location since it has a mapset - self._reloadLocationNode(location_node) - self._model.SortChildren(grassdb_node) - self.RefreshNode(grassdb_node, recursive=True) - return location_node + """ + Insert new location into model and refresh tree. + Check if not already added. + """ + if not self.GetDbNode(grassdb=grassdb_node.label, location=name): + location_node = self._model.AppendNode( + parent=grassdb_node, data={"type": "location", "name": name} + ) + # reload new location since it has a mapset + self._reloadLocationNode(location_node) + self._model.SortChildren(grassdb_node) + self.RefreshNode(grassdb_node, recursive=True) + return location_node def InsertGrassDb(self, name): """ @@ -1819,6 +1842,7 @@ def _updateAfterGrassdbChanged( # instead the watchdog handler takes care of refreshing tree if ( watchdog_used + and self._watchingMapsetEnabled and grassdb == self.current_grassdb_node.data["name"] and location == self.current_location_node.data["name"] and mapset == self.current_mapset_node.data["name"] diff --git a/gui/wxpython/dbmgr/base.py b/gui/wxpython/dbmgr/base.py index d743a74e3c7..1887a20f13a 100644 --- a/gui/wxpython/dbmgr/base.py +++ b/gui/wxpython/dbmgr/base.py @@ -429,13 +429,21 @@ def AddDataRow(self, i, record, columns, keyId): if keyId > -1: # load cats only when LoadData() is called first time self.itemCatsMap[i] = cat + def GetMapCatKeyColumn(self): + """Get map cat key column name and index + + :return dict: e.g. {"name": "cat", "index": 0} + """ + cols = self.mapDBInfo.GetColumns(self.mapDBInfo.layers[self.layer]["table"]) + catColName = self.mapDBInfo.layers[self.layer]["key"] + return {"name": catColName, "index": cols.index(catColName)} + def OnItemSelected(self, event): """Item selected. Add item to selected cats...""" # cat = int(self.GetItemText(event.m_itemIndex)) # if cat not in self.selectedCats: # self.selectedCats.append(cat) # self.selectedCats.sort() - event.Skip() def OnItemDeselected(self, event): @@ -444,15 +452,15 @@ def OnItemDeselected(self, event): # if cat in self.selectedCats: # self.selectedCats.remove(cat) # self.selectedCats.sort() - event.Skip() def GetSelectedItems(self): """Return list of selected items (category numbers)""" cats = [] item = self.GetFirstSelected() + catColIdx = self.GetMapCatKeyColumn()["index"] while item != -1: - cats.append(self.GetItemText(item)) + cats.append(self.GetItemText(item=item, col=catColIdx)) item = self.GetNextSelected(item) return cats @@ -460,8 +468,9 @@ def GetSelectedItems(self): def GetItems(self): """Return list of items (category numbers)""" cats = [] + catColIdx = self.GetMapCatKeyColumn()["index"] for item in range(self.GetItemCount()): - cats.append(self.GetItemText(item)) + cats.append(self.GetItemText(item=item, col=catColIdx)) return cats @@ -3925,7 +3934,9 @@ def __init__(self, parent, id=wx.ID_ANY, style=wx.DEFAULT_FRAME_STYLE, **kwargs) self.SetTitle(_("Field statistics")) self.SetIcon( - wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"), wx.BITMAP_TYPE_ICO) + wx.Icon( + os.path.join(globalvar.ICONDIR, "grass_sql.ico"), wx.BITMAP_TYPE_ICO + ) ) self.panel = wx.Panel(parent=self, id=wx.ID_ANY) @@ -4009,18 +4020,67 @@ def Update(self, driver, database, table, column): self.Close() return + # Get cat key col name + catColName = self.parent.GetMapCatKeyColumn()["name"] + + eol = "\n" + sqlWhereClause = "" + baseSql = "select {fn}({column}) from {table} {where};{eol}" + + # Interactively selected rows + interactivelySelectedCats = self.parent.GetSelectedItems() + # SQL filtered rows + sqlFilteredCats = [] + + if self.parent.sqlFilter: + sqlFilteredCats = self.parent.GetItems() + + cats = None + if len(interactivelySelectedCats) > 0 and len(sqlFilteredCats) == 0: + cats = ", ".join(interactivelySelectedCats) + elif len(interactivelySelectedCats) > 0 and len(sqlFilteredCats) > 0: + cats = ", ".join(set(interactivelySelectedCats) & set(sqlFilteredCats)) + elif len(interactivelySelectedCats) == 0 and len(sqlFilteredCats) > 0: + cats = ", ".join(sqlFilteredCats) + + if cats: + sqlWhereClause = f" where {catColName} in ({cats})" + fd, sqlFilePath = tempfile.mkstemp(text=True) stats = ["count", "min", "max", "avg", "sum", "null"] with open(sqlFilePath, "w") as sqlFile: for fn in stats: if fn == "null": - sqlFile.write( - "select count(*) from %s where %s is null;%s" - % (table, column, "\n") - ) + sql = "select count(*) from {table} where {column} is null {andOperatorCondition};{eol}" + # Appending SQL AND operator condition + if sqlWhereClause: + sqlFile.write( + sql.format( + table=table, + column=column, + andOperatorCondition=f" and {catColName} in ({cats})", + eol=eol, + ) + ) + else: + sqlFile.write( + sql.format( + table=table, + column=column, + andOperatorCondition="", + eol=eol, + ) + ) else: + # Appending SQL WHERE clause with IN operator sqlFile.write( - "select %s(%s) from %s;%s" % (fn, column, table, "\n") + baseSql.format( + fn=fn, + column=column, + table=table, + where=sqlWhereClause, + eol=eol, + ) ) dataStr = RunCommand( diff --git a/gui/wxpython/dbmgr/dialogs.py b/gui/wxpython/dbmgr/dialogs.py index 52168f6220c..000f2b8a1a1 100644 --- a/gui/wxpython/dbmgr/dialogs.py +++ b/gui/wxpython/dbmgr/dialogs.py @@ -24,7 +24,7 @@ from core.gcmd import RunCommand, GError from core.debug import Debug from dbmgr.vinfo import VectorDBInfo, GetUnicodeValue, GetDbEncoding -from gui_core.widgets import IntegerValidator, FloatValidator +from gui_core.widgets import IntegerValidator, FloatValidator, TimeISOValidator from gui_core.wrap import SpinCtrl, Button, StaticText, StaticBox, TextCtrl @@ -664,6 +664,11 @@ def __init__( valueWin.SetValidator(IntegerValidator()) elif ctype == float: valueWin.SetValidator(FloatValidator()) + elif ctype == str and ctypeStr == "date": + valueWin.SetValidator(TimeISOValidator()) + # Date ISO8601 format hint + valueWin.SetHint(_("YYYY-MM-DD")) + if not winFocus: wx.CallAfter(valueWin.SetFocus) winFocus = True diff --git a/gui/wxpython/dbmgr/g.gui.dbmgr.py b/gui/wxpython/dbmgr/g.gui.dbmgr.py index ba96b3d44c1..b1aa8be3a98 100755 --- a/gui/wxpython/dbmgr/g.gui.dbmgr.py +++ b/gui/wxpython/dbmgr/g.gui.dbmgr.py @@ -52,7 +52,7 @@ def main(): f = AttributeManager( parent=None, id=wx.ID_ANY, - base_title=_("Attribute Table Manager - GRASS GIS"), + base_title=_("Attribute Table Manager - GRASS"), size=(900, 600), vectorName=mapName, ) diff --git a/gui/wxpython/docs/wxGUI.html b/gui/wxpython/docs/wxGUI.html index 70643d6b24f..ca57ab35a57 100644 --- a/gui/wxpython/docs/wxGUI.html +++ b/gui/wxpython/docs/wxGUI.html @@ -2,8 +2,8 @@

DESCRIPTION

wxGUI is a native Graphical User Interface (GUI) for -GRASS GIS. Its main features include displaying geographical data -in 2D and 3D, calling GRASS GIS modules, and interacting with data. +GRASS. Its main features include displaying geographical data +in 2D and 3D, calling GRASS modules, and interacting with data.

Overview

The GUI is composed of three main components: @@ -29,7 +29,7 @@

Layer Manager

map layers, a layer tree frame in which map layers for display are organized, a command output window tab, and interactive command line prompt. On Linux and Windows platforms, the layer manager also has a menu bar with a set of -pull-down menus for all GRASS GIS functions (analysis, file I/O, GIS +pull-down menus for all GRASS functions (analysis, file I/O, GIS configuration and management); on a Mac, the GRASS functions menu is at the top of the screen. @@ -209,7 +209,7 @@

Layer Manager Toolbar

icon  Add command layer
-
Adds a layer in which a GRASS GIS command or command list can be entered. +
Adds a layer in which a GRASS command or command list can be entered. For a command list use the semi-colon (";") symbol as a separator. For example: @@ -650,7 +650,7 @@

Starting the GUI from command line

If the wxGUI is not the default user interface, it can defined as default by -typing at the GRASS GIS command line: +typing at the GRASS command line:
 g.gui -d wxpython
@@ -677,16 +677,16 @@ 

Starting the GUI from command line

-The GUI can be quit by selecting the 'File > Quit GRASS GIS' menu item -which gives options to close only GUI or to quit GRASS GIS entirely -if GRASS GIS is running with a command line (a shell in a terminal application). +The GUI can be quit by selecting the 'File > Quit GRASS' menu item +which gives options to close only GUI or to quit GRASS entirely +if GRASS is running with a command line (a shell in a terminal application). Exiting the shell (typically by the exit command) ends the GRASS session including any running GUIs.

Background information

wxGUI is a native Graphical User Interface (GUI) for -GRASS GIS written in Python +GRASS written in Python using wxPython library.

SEE ALSO

diff --git a/gui/wxpython/docs/wxGUI.modules.html b/gui/wxpython/docs/wxGUI.modules.html index 7e8125f7443..f895f3657d7 100644 --- a/gui/wxpython/docs/wxGUI.modules.html +++ b/gui/wxpython/docs/wxGUI.modules.html @@ -7,7 +7,7 @@

KEYWORDS

DESCRIPTION

-GRASS GIS functionality is organized into modules, which are standalone programs +GRASS functionality is organized into modules, which are standalone programs with defined interface. Their graphical user interface (GUI) is a dialog with several tabs which organize module parameters into groups.

diff --git a/gui/wxpython/docs/wxGUI.toolboxes.html b/gui/wxpython/docs/wxGUI.toolboxes.html index 8679a8eab31..edf56c93b55 100644 --- a/gui/wxpython/docs/wxGUI.toolboxes.html +++ b/gui/wxpython/docs/wxGUI.toolboxes.html @@ -42,7 +42,7 @@

Brief description of file main_menu.xml

 <?xml version="1.0" encoding="UTF-8"?>
 <toolbox name="MyMainMenu">
-  <label>Default GRASS GIS main menu bar</label>
+  <label>Default GRASS main menu bar</label>
   <items>
     <subtoolbox name="File"/>
     <subtoolbox name="Settings"/>
@@ -116,7 +116,7 @@ 

Hiding menu items

 <?xml version="1.0" encoding="UTF-8"?>
 <toolbox name="CustomizedMainMenu">
-  <label>Default GRASS GIS main menu bar</label>
+  <label>Default GRASS main menu bar</label>
   <items>
     <subtoolbox name="File"/>
     <subtoolbox name="Settings"/>
@@ -167,7 +167,7 @@ 

Creating custom toolbox

 <?xml version="1.0" encoding="UTF-8"?>
 <toolbox name="CustomizedMainMenu">
-  <label>Default GRASS GIS main menu bar</label>
+  <label>Default GRASS main menu bar</label>
   <items>
     <subtoolbox name="File"/>
     <subtoolbox name="Settings"/>
diff --git a/gui/wxpython/docs/wxgui_sphinx/conf.py b/gui/wxpython/docs/wxgui_sphinx/conf.py
index 1d38959fcc9..8949952259f 100644
--- a/gui/wxpython/docs/wxgui_sphinx/conf.py
+++ b/gui/wxpython/docs/wxgui_sphinx/conf.py
@@ -95,7 +95,7 @@
 
 # The reST default role (used for this markup: `text`) to use for all
 # documents.
-# default_role = None
+default_role = "literal"
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
 # add_function_parentheses = True
diff --git a/gui/wxpython/gcp/g.gui.gcp.html b/gui/wxpython/gcp/g.gui.gcp.html
index 97713d9a763..00c81ca0fd6 100644
--- a/gui/wxpython/gcp/g.gui.gcp.html
+++ b/gui/wxpython/gcp/g.gui.gcp.html
@@ -277,7 +277,7 @@ 

Toolbar for the GCP list

GCP Map Display Statusbar

The GCP map display statusbar is similar to the statusbar in the regular -GRASS GIS map display with two differences, Go to has been +GRASS map display with two differences, Go to has been replaced with Go to GCP No. and Projection has been replaced with RMS error.

diff --git a/gui/wxpython/gcp/g.gui.gcp.md b/gui/wxpython/gcp/g.gui.gcp.md index 2353d2d44e3..b91ca9530a7 100644 --- a/gui/wxpython/gcp/g.gui.gcp.md +++ b/gui/wxpython/gcp/g.gui.gcp.md @@ -237,7 +237,7 @@ imagery group. #### GCP Map Display Statusbar The GCP map display statusbar is similar to the statusbar in the regular -GRASS GIS map display with two differences, *Go to* has been replaced +GRASS map display with two differences, *Go to* has been replaced with *Go to GCP No.* and *Projection* has been replaced with *RMS error*. diff --git a/gui/wxpython/gcp/manager.py b/gui/wxpython/gcp/manager.py index 2c5e7cee9ee..60dd4c1fa93 100644 --- a/gui/wxpython/gcp/manager.py +++ b/gui/wxpython/gcp/manager.py @@ -1,7 +1,7 @@ """ @package gcp.manager -@brief Georectification module for GRASS GIS. Includes ground control +@brief Georectification module for GRASS. Includes ground control point management and interactive point and click GCP creation Classes: @@ -647,7 +647,7 @@ def OnVGroup(self, event): self.grassdatabase, self.xylocation, self.xymapset, "vector" ) - if os.path.exists(vector_dir): + if Path(vector_dir).exists(): dlg = VectGroup( parent=self, id=wx.ID_ANY, @@ -1104,7 +1104,7 @@ def __init__( } # make a backup of the current points file - if os.path.exists(self.file["points"]): + if Path(self.file["points"]).exists(): shutil.copy(self.file["points"], self.file["points_bak"]) # polynomial order transformation for georectification @@ -1928,10 +1928,10 @@ def OnQuit(self, event): self.SaveGCPs(None) elif ret == wx.NO: # restore POINTS file from backup - if os.path.exists(self.file["points_bak"]): + if Path(self.file["points_bak"]).exists(): shutil.copy(self.file["points_bak"], self.file["points"]) - if os.path.exists(self.file["points_bak"]): + if Path(self.file["points_bak"]).exists(): os.unlink(self.file["points_bak"]) self.SrcMap.Clean() @@ -2411,7 +2411,7 @@ def LoadData(self): self.DeleteAllItems() self.render = False - if os.path.isfile(self.gcp.file["points"]): + if Path(self.gcp.file["points"]).is_file(): self.gcp.ReadGCPs() else: # 3 gcp is minimum @@ -2636,7 +2636,7 @@ def __init__( # self.listMap = CheckListBox(parent=self, id=wx.ID_ANY, choices=vectlist) - if os.path.isfile(self.vgrpfile): + if Path(self.vgrpfile).is_file(): with open(self.vgrpfile) as f: checked = [] for line in f: @@ -2705,8 +2705,7 @@ def MakeVGroup(self): dirname = os.path.dirname(self.vgrpfile) - if not os.path.exists(dirname): - os.makedirs(dirname) + Path(dirname).mkdir(parents=True, exist_ok=True) with open(self.vgrpfile, mode="w") as f: f.writelines(vect + "\n" for vect in vgrouplist) diff --git a/gui/wxpython/gmodeler/g.gui.gmodeler.html b/gui/wxpython/gmodeler/g.gui.gmodeler.html index 0fcd15e1a4c..9b92a3ec3c2 100644 --- a/gui/wxpython/gmodeler/g.gui.gmodeler.html +++ b/gui/wxpython/gmodeler/g.gui.gmodeler.html @@ -6,21 +6,21 @@

DESCRIPTION

The Graphical Modeler is a wxGUI component which allows the user to create, edit, and manage simple and complex models using an easy-to-use interface. -When performing analytical operations in GRASS GIS, the +When performing analytical operations in GRASS, the operations are not isolated, but part of a chain of operations. Using the -Graphical Modeler, a chain of processes (i.e. GRASS GIS modules) +Graphical Modeler, a chain of processes (i.e. GRASS modules) can be wrapped into one process (i.e. model). Subsequently it is easier to execute the model later on even with slightly different inputs or parameters.
-Models represent a programming technique used in GRASS GIS to +Models represent a programming technique used in GRASS to concatenate single steps together to accomplish a task. It is advantageous when the user see boxes and ovals that are connected by lines and represent some tasks rather than seeing lines of coded text. The Graphical Modeler can be used as a custom tool that automates a process. Created models can simplify or shorten a task which can be run many times and it can also be easily shared with others. Important to note is that models cannot -perform specified tasks that one cannot also manually perform with GRASS -GIS. It is recommended to first to develop the process manually, note down +perform specified tasks that one cannot also manually perform with GRASS. +It is recommended to first to develop the process manually, note down the steps (e.g. by using the Copy button in module dialogs) and later replicate them in model. @@ -319,7 +319,7 @@

Defining variables

Saving the model file

-Finally, the model settings can be stored as a GRASS GIS Model file with +Finally, the model settings can be stored as a GRASS Model file with *.gxm extension. The advantage is that it can be shared as a reusable workflow that may be run also by other users with different data. @@ -396,7 +396,7 @@

Defining loops

In the example below the MODIS MOD13Q1 (NDVI) satellite data products are used in a loop. The original data are stored as coded integer values that need to be multiplied by the -value 0.0001 to represent real ndvi values. Moreover, GRASS GIS +value 0.0001 to represent real ndvi values. Moreover, GRASS provides a predefined color table called ndvi to represent ndvi data. In this case it is not necessary to work with every image separately.
@@ -410,7 +410,7 @@

Defining loops


-Figure: MODIS data representation in GRASS GIS after Graphical Modeler usage. +Figure: MODIS data representation in GRASS after Graphical Modeler usage.

@@ -439,7 +439,7 @@

Defining loops


-Figure: MODIS data representation in GRASS GIS after Graphical Modeler usage. +Figure: MODIS data representation in GRASS after Graphical Modeler usage.

diff --git a/gui/wxpython/gmodeler/g.gui.gmodeler.md b/gui/wxpython/gmodeler/g.gui.gmodeler.md index 5d2dca14697..b19606c19f9 100644 --- a/gui/wxpython/gmodeler/g.gui.gmodeler.md +++ b/gui/wxpython/gmodeler/g.gui.gmodeler.md @@ -10,12 +10,12 @@ index: topic_GUI|GUI The **Graphical Modeler** is a *[wxGUI](wxGUI.md)* component which allows the user to create, edit, and manage simple and complex models using an easy-to-use interface. When performing analytical operations in -GRASS GIS, the operations are not isolated, but part of a chain of +GRASS, the operations are not isolated, but part of a chain of operations. Using the Graphical Modeler, a chain of processes (i.e. -GRASS GIS modules) can be wrapped into one process (i.e. model). +GRASS modules) can be wrapped into one process (i.e. model). Subsequently it is easier to execute the model later on even with slightly different inputs or parameters. -Models represent a programming technique used in GRASS GIS to +Models represent a programming technique used in GRASS to concatenate single steps together to accomplish a task. It is advantageous when the user see boxes and ovals that are connected by lines and represent some tasks rather than seeing lines of coded text. @@ -23,7 +23,7 @@ The Graphical Modeler can be used as a custom tool that automates a process. Created models can simplify or shorten a task which can be run many times and it can also be easily shared with others. Important to note is that models cannot perform specified tasks that one cannot also -manually perform with GRASS GIS. It is recommended to first to develop +manually perform with GRASS. It is recommended to first to develop the process manually, note down the steps (e.g. by using the *Copy* button in module dialogs) and later replicate them in model. @@ -257,7 +257,7 @@ Then it should be set in the module dialog as input called `%{raster}`. ### Saving the model file -Finally, the model settings can be stored as a GRASS GIS Model file with +Finally, the model settings can be stored as a GRASS Model file with `*.gxm` extension. The advantage is that it can be shared as a reusable workflow that may be run also by other users with different data. @@ -321,7 +321,7 @@ In the example below the [MODIS MOD13Q1](https://e4ftl01.cr.usgs.gov/MOLT/MOD13Q1.006/) (NDVI) satellite data products are used in a loop. The original data are stored as coded integer values that need to be multiplied by the value `0.0001` to -represent real *ndvi values*. Moreover, GRASS GIS provides a predefined +represent real *ndvi values*. Moreover, GRASS provides a predefined color table called `ndvi` to represent *ndvi data*. In this case it is not necessary to work with every image separately. The Graphical Modeler is an appropriate tool to process data in an @@ -330,9 +330,9 @@ image in mapset and `%{ndvi}` for original data name suffix). After the loop component is added to model, it is necessary to define series of maps with required settings of map type, mapset, etc. -![MODIS data representation in GRASS GIS after Graphical Modeler +![MODIS data representation in GRASS after Graphical Modeler usage](g_gui_gmodeler_loop_dlg.png) -*Figure: MODIS data representation in GRASS GIS after Graphical Modeler +*Figure: MODIS data representation in GRASS after Graphical Modeler usage.* When the model is supplemented by all of modules, these modules should @@ -342,13 +342,13 @@ are shown below. ![Model with loop](g_gui_gmodeler_loop_final.png) *Figure: Model with loop.* -![MODIS data representation in GRASS GIS](g_gui_gmodeler_modis_1o.png) -![MODIS data representation in GRASS GIS](g_gui_gmodeler_modis_1.png) -![MODIS data representation in GRASS GIS](g_gui_gmodeler_modis_2o.png) -![MODIS data representation in GRASS GIS](g_gui_gmodeler_modis_2.png) -![MODIS data representation in GRASS GIS](g_gui_gmodeler_modis_3o.png) -![MODIS data representation in GRASS GIS](g_gui_gmodeler_modis_3.png) -*Figure: MODIS data representation in GRASS GIS after Graphical Modeler +![MODIS data representation in GRASS](g_gui_gmodeler_modis_1o.png) +![MODIS data representation in GRASS](g_gui_gmodeler_modis_1.png) +![MODIS data representation in GRASS](g_gui_gmodeler_modis_2o.png) +![MODIS data representation in GRASS](g_gui_gmodeler_modis_2.png) +![MODIS data representation in GRASS](g_gui_gmodeler_modis_3o.png) +![MODIS data representation in GRASS](g_gui_gmodeler_modis_3.png) +*Figure: MODIS data representation in GRASS after Graphical Modeler usage.* The steps to enter in the command console of the Graphical Modeler would diff --git a/gui/wxpython/gmodeler/g.gui.gmodeler.py b/gui/wxpython/gmodeler/g.gui.gmodeler.py index 01d15afbcfa..02399776cb9 100755 --- a/gui/wxpython/gmodeler/g.gui.gmodeler.py +++ b/gui/wxpython/gmodeler/g.gui.gmodeler.py @@ -53,7 +53,7 @@ def main(): frame = ModelerFrame( parent=None, giface=StandaloneGrassInterface(), - title=_("Graphical Modeler - GRASS GIS"), + title=_("Graphical Modeler - GRASS"), ) if options["file"]: frame.panel.LoadModelFile(options["file"]) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index dcfae3d3ed5..506f6169305 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -901,7 +901,7 @@ def OnModelSaveAs(self, event=None): if filename[-4:] != ".gxm": filename += ".gxm" - if os.path.exists(filename): + if Path(filename).exists(): dlg = wx.MessageDialog( parent=self, message=_( @@ -1742,7 +1742,7 @@ def SaveAs(self, force=False): if filename[-len(file_ext) - 1 :] != f".{file_ext}": filename += f".{file_ext}" - if os.path.exists(filename): + if Path(filename).exists(): dlg = wx.MessageDialog( self, message=_( diff --git a/gui/wxpython/gui_core/dialogs.py b/gui/wxpython/gui_core/dialogs.py index 7cec1bda7d2..985dcad9338 100644 --- a/gui/wxpython/gui_core/dialogs.py +++ b/gui/wxpython/gui_core/dialogs.py @@ -2331,7 +2331,7 @@ class QuitDialog(wx.Dialog): def __init__( self, parent, - title=_("Quit GRASS GIS"), + title=_("Quit GRASS"), id=wx.ID_ANY, style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, **kwargs, @@ -2352,19 +2352,15 @@ def __init__( self._shell_running = is_shell_running() if self._shell_running: - text = _( - "Do you want to quit GRASS GIS including shell or just close the GUI?" - ) + text = _("Do you want to quit GRASS including shell or just close the GUI?") else: - text = _("Do you want to quit GRASS GIS?") + text = _("Do you want to quit GRASS?") self.informLabel = StaticText(parent=self.panel, id=wx.ID_ANY, label=text) self.btnCancel = Button(parent=self.panel, id=wx.ID_CANCEL) if self._shell_running: self.btnClose = Button(parent=self.panel, id=wx.ID_NO, label=_("Close GUI")) self.btnClose.Bind(wx.EVT_BUTTON, self.OnClose) - self.btnQuit = Button( - parent=self.panel, id=wx.ID_YES, label=_("Quit GRASS GIS") - ) + self.btnQuit = Button(parent=self.panel, id=wx.ID_YES, label=_("Quit GRASS")) self.btnQuit.SetFocus() self.btnQuit.Bind(wx.EVT_BUTTON, self.OnQuit) diff --git a/gui/wxpython/gui_core/forms.py b/gui/wxpython/gui_core/forms.py index 4e0b3b93857..fe76a652f6e 100644 --- a/gui/wxpython/gui_core/forms.py +++ b/gui/wxpython/gui_core/forms.py @@ -72,9 +72,7 @@ if __name__ == "__main__": if os.getenv("GISBASE") is None: # intentionally not translatable - sys.exit( - "Failed to start. GRASS GIS is not running or the installation is broken." - ) + sys.exit("Failed to start. GRASS is not running or the installation is broken.") from grass.script.setup import set_gui_path set_gui_path() @@ -103,6 +101,7 @@ FormListbook, FormNotebook, PlacementValidator, + MapNameValidator, ) from core.giface import Notification, StandaloneGrassInterface from gui_core.widgets import LayersList @@ -1511,6 +1510,9 @@ def __init__(self, parent, giface, task, id=wx.ID_ANY, frame=None, *args, **kwar selection = gselect.Select( parent=which_panel, id=wx.ID_ANY, + validator=MapNameValidator() + if p.get("age") == "new" + else wx.DefaultValidator, size=globalvar.DIALOG_GSELECT_SIZE, type=elem, multiple=multiple, @@ -2035,7 +2037,7 @@ def __init__(self, parent, giface, task, id=wx.ID_ANY, frame=None, *args, **kwar style=wx.TE_MULTILINE, size=(-1, 75), ) - if p.get("value", "") and os.path.isfile(p["value"]): + if p.get("value", "") and Path(p["value"]).is_file(): ifbb.Clear() try: # Python >= 3.11 @@ -3352,7 +3354,8 @@ def OnInit(self): "gisprompt": False, "multiple": "yes", # values must be an array of strings - "values": utils.str2rgb.keys() + list(map(str, utils.str2rgb.values())), + "values": list(utils.str2rgb.keys()) + + list(map(str, utils.str2rgb.values())), "key_desc": ["value"], "values_desc": [], }, diff --git a/gui/wxpython/gui_core/ghelp.py b/gui/wxpython/gui_core/ghelp.py index bc4924ecb80..6ab7e000909 100644 --- a/gui/wxpython/gui_core/ghelp.py +++ b/gui/wxpython/gui_core/ghelp.py @@ -16,6 +16,7 @@ @author Martin Landa """ +from __future__ import annotations import os import codecs import platform @@ -61,7 +62,7 @@ class AboutWindow(wx.Frame): """Create custom About Window""" - def __init__(self, parent, size=(770, 460), title=_("About GRASS GIS"), **kwargs): + def __init__(self, parent, size=(770, 460), title=_("About GRASS"), **kwargs): wx.Frame.__init__( self, parent=parent, id=wx.ID_ANY, title=title, size=size, **kwargs ) @@ -131,7 +132,7 @@ def _pageInfo(self): logoBitmap, proportion=0, flag=wx.ALL | wx.ALIGN_CENTER, border=20 ) - infoLabel = "GRASS GIS %s" % vInfo.get("version", _("unknown version")) + infoLabel = "GRASS %s" % vInfo.get("version", _("unknown version")) if "x86_64" in vInfo.get("build_platform", ""): infoLabel += " (64bit)" info = StaticText(parent=infoTxt, id=wx.ID_ANY, label=infoLabel + os.linesep) @@ -276,7 +277,7 @@ def _pageInfo(self): def _pageCopyright(self): """Copyright information""" copyfile = os.path.join(os.getenv("GISBASE"), "COPYING") - if os.path.exists(copyfile): + if Path(copyfile).exists(): copytext = Path(copyfile).read_text() else: copytext = _("%s file missing") % "COPYING" @@ -303,7 +304,7 @@ def _pageCopyright(self): def _pageLicense(self): """Licence about""" licfile = os.path.join(os.getenv("GISBASE"), "GPL.TXT") - if os.path.exists(licfile): + if Path(licfile).exists(): with open(licfile) as licenceFile: license = "".join(licenceFile.readlines()) else: @@ -334,7 +335,7 @@ def _pageCitation(self): except CalledModuleError as error: text = _( "Unable to provide citation suggestion," - " see GRASS GIS website instead." + " see GRASS website instead." " The error was: {0}" ).format(error) @@ -356,7 +357,7 @@ def _pageCredit(self): """Credit about""" # credits authfile = os.path.join(os.getenv("GISBASE"), "AUTHORS") - if os.path.exists(authfile): + if Path(authfile).exists(): with codecs.open(authfile, encoding="utf-8", mode="r") as authorsFile: authors = "".join(authorsFile.readlines()) else: @@ -383,7 +384,7 @@ def _pageContributors(self, extra=False): contribfile = os.path.join(os.getenv("GISBASE"), "contributors_extra.csv") else: contribfile = os.path.join(os.getenv("GISBASE"), "contributors.csv") - if os.path.exists(contribfile): + if Path(contribfile).exists(): contribs = [] errLines = [] with codecs.open(contribfile, encoding="utf-8", mode="r") as contribFile: @@ -466,7 +467,7 @@ def _pageContributors(self, extra=False): def _pageTranslators(self): """Translators info""" translatorsfile = os.path.join(os.getenv("GISBASE"), "translators.csv") - if os.path.exists(translatorsfile): + if Path(translatorsfile).exists(): translators = {} errLines = [] with codecs.open(translatorsfile, encoding="utf-8", mode="r") as fd: @@ -537,7 +538,7 @@ def _pageTranslators(self): flag = os.path.join( globalvar.ICONDIR, "flags", "%s.png" % lang.lower() ) - if os.path.exists(flag): + if Path(flag).exists(): flagBitmap = wx.StaticBitmap( translatorswin, wx.ID_ANY, @@ -646,7 +647,7 @@ def _pageStats(self): """Translation statistics info""" fname = "translation_status.json" statsfile = os.path.join(os.getenv("GISBASE"), fname) - if os.path.exists(statsfile): + if Path(statsfile).exists(): import json with open(statsfile) as statsFile: @@ -698,12 +699,12 @@ def __init__(self): self.base_url = base_url self.target_class = "md-content" - def handle_starttag(self, tag, attrs): + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]): attr_dict = dict(attrs) # Convert relative URLs to absolute # to be able to display images and use links - if tag in ["img", "a"]: + if tag in {"img", "a"}: if "src" in attr_dict and not bool(urlparse(attr_dict["src"]).netloc): attr_dict["src"] = urljoin(self.base_url, attr_dict["src"]) if "href" in attr_dict and not bool(urlparse(attr_dict["href"]).netloc): @@ -722,7 +723,7 @@ def handle_starttag(self, tag, attrs): if tag == "div": self.depth += 1 - def handle_endtag(self, tag): + def handle_endtag(self, tag: str): if self.recording: self.extracted_data.append(f"") if tag == "div": @@ -730,7 +731,7 @@ def handle_endtag(self, tag): if self.depth == 0: self.recording = False - def handle_data(self, data): + def handle_data(self, data: str): if self.recording: self.extracted_data.append(data) @@ -762,7 +763,7 @@ def __init__(self, parent, command, text, **kwargs): self.markdown = True # check if mkdocs is used (add slash at the end) self.fspath = os.path.join(os.getenv("GISBASE"), "docs", "mkdocs", "site", "") - if not os.path.exists(self.fspath): + if not Path(self.fspath).exists(): self.markdown = False self.fspath = os.path.join(os.getenv("GISBASE"), "docs", "html", "") @@ -864,14 +865,14 @@ def LoadPage(self, path=None): def GetFile(self): """Get HTML file""" fMan = os.path.join(self.content.fspath, self.command + ".html") - if os.path.isfile(fMan): + if Path(fMan).is_file(): return fMan # check also addons faMan = os.path.join( os.getenv("GRASS_ADDON_BASE"), "docs", "html", self.command + ".html" ) - if os.getenv("GRASS_ADDON_BASE") and os.path.isfile(faMan): + if os.getenv("GRASS_ADDON_BASE") and Path(faMan).is_file(): return faMan return None diff --git a/gui/wxpython/gui_core/gselect.py b/gui/wxpython/gui_core/gselect.py index c5d89b56e0d..b26ed75e07e 100644 --- a/gui/wxpython/gui_core/gselect.py +++ b/gui/wxpython/gui_core/gselect.py @@ -1158,7 +1158,7 @@ def InsertColumns( :param excludeCols: list of columns to be removed from the list :param type: only columns of given type (given as list) :param dbInfo: dbInfo object - :param setDefaultValue: True to set default value + :param bool setDefaultValue: True to set default value """ if not dbInfo: dbInfo = VectorDBInfo(vector) @@ -1204,12 +1204,15 @@ def InsertColumns( if value != "" and value in self.columns: self.SetValue(value) - def InsertTableColumns(self, table, driver=None, database=None): + def InsertTableColumns( + self, table, driver=None, database=None, setDefaultValue=True + ): """Insert table columns :param str table: table name :param str driver: driver name :param str database: database name + :param bool setDefaultValue: True to set default value """ self.columns[:] = [] @@ -1222,7 +1225,8 @@ def InsertTableColumns(self, table, driver=None, database=None): # update list self.tcp.DeleteAllItems() - self.SetValue(self.defaultValue) + if setDefaultValue: + self.SetValue(self.defaultValue) for col in self.columns: self.tcp.AddItem(col) @@ -2640,6 +2644,8 @@ def _getRasterliteDBRasters(self, dsn): """ try: from osgeo import gdal + + gdal.DontUseExceptions() except ImportError: GError( parent=self, diff --git a/gui/wxpython/gui_core/infobar.py b/gui/wxpython/gui_core/infobar.py index e9bd8b848a5..cf6ea07c0f4 100644 --- a/gui/wxpython/gui_core/infobar.py +++ b/gui/wxpython/gui_core/infobar.py @@ -18,7 +18,6 @@ import sys import wx -import wx.aui try: import wx.lib.agw.infobar as IB diff --git a/gui/wxpython/gui_core/menu.py b/gui/wxpython/gui_core/menu.py index 2f462625dbd..876b56a4ec8 100644 --- a/gui/wxpython/gui_core/menu.py +++ b/gui/wxpython/gui_core/menu.py @@ -25,6 +25,7 @@ import re import os +from pathlib import Path import wx from core import globalvar @@ -357,7 +358,7 @@ def __init__(self, app_name, parent_menu, pos, history_len=10): self.file_requested = Signal("RecentFilesMenu.FileRequested") self._filehistory = wx.FileHistory(maxFiles=history_len) - # Recent files path stored in GRASS GIS config dir in the + # Recent files path stored in GRASS config dir in the # .recent_files file in the group by application name self._config = wx.FileConfig( style=wx.CONFIG_USE_LOCAL_FILE, @@ -402,7 +403,7 @@ def _onFileHistory(self, event): file_index = event.GetId() - wx.ID_FILE1 path = self._filehistory.GetHistoryFile(file_index) - if not os.path.exists(path): + if not Path(path).exists(): self.RemoveFileFromHistory(file_index) file_exists = False self.file_requested.emit( @@ -442,7 +443,7 @@ def RemoveNonExistentFiles(self): """Remove non existent files from the history""" for i in reversed(range(self._filehistory.GetCount())): file = self._filehistory.GetHistoryFile(index=i) - if not os.path.exists(file): + if not Path(file).exists(): self._filehistory.RemoveFileFromHistory(i=i) self._filehistory.Save(self._config) diff --git a/gui/wxpython/gui_core/prompt.py b/gui/wxpython/gui_core/prompt.py index 3857f04fb32..e14dd7da413 100644 --- a/gui/wxpython/gui_core/prompt.py +++ b/gui/wxpython/gui_core/prompt.py @@ -458,13 +458,13 @@ def OnKeyPressed(self, event): self.CallTipSetForeground("BLACK") self.CallTipShow(pos, info["usage"] + "\n\n" + info["description"]) elif ( - event.GetKeyCode() in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER) + event.GetKeyCode() in {wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER} and not self.AutoCompActive() ): # run command on line when is pressed self._runCmd(self.GetCurLine()[0].strip()) elif ( - event.GetKeyCode() in [wx.WXK_UP, wx.WXK_DOWN] and not self.AutoCompActive() + event.GetKeyCode() in {wx.WXK_UP, wx.WXK_DOWN} and not self.AutoCompActive() ): # Command history using up and down if len(self.cmdbuffer) < 1: diff --git a/gui/wxpython/gui_core/pyedit.py b/gui/wxpython/gui_core/pyedit.py index 93c20070d5e..fe07770b89a 100644 --- a/gui/wxpython/gui_core/pyedit.py +++ b/gui/wxpython/gui_core/pyedit.py @@ -1,9 +1,9 @@ -"""GRASS GIS Simple Python Editor +"""GRASS Simple Python Editor Copyright (C) 2016 by the GRASS Development Team This program is free software under the GNU General Public -License (>=v2). Read the file COPYING that comes with GRASS GIS +License (>=v2). Read the file COPYING that comes with GRASS for details. :authors: Vaclav Petras @@ -284,6 +284,7 @@ def __init__(self, panel, guiparent, giface): self.tempfile = None # bool, make them strings for better code self.overwrite = False self.parameters = None + self.recent_files = None def _openFile(self, file_path): """Try open file and read content @@ -411,7 +412,7 @@ def SaveAs(self): if filename[-3:] != ".py": filename += ".py" - if os.path.exists(filename): + if Path(filename).exists(): dlg = wx.MessageDialog( parent=self.guiparent, message=_( @@ -795,9 +796,15 @@ def _layout(self): self.Layout() - def SetUpPage(self, *args, **kwargs): - MainPageBase.SetUpPage(self, *args, **kwargs) - self.controller.SetRecentFilesMenu(self.GetMenu()[0]) + def OnDockUndock(self, event=None): + MainPageBase.OnDockUndock(self, event) + if not MainPageBase.IsDocked(self): + if self.controller.recent_files is None: + menuBar = self.GetMenu()[0] + # Init recent files submenu + self.controller.SetRecentFilesMenu( + menu=menuBar.GetMenus()[0][0], # Parent File menu + ) # TODO: it would be nice if we can pass the controller to the menu # might not be possible on the side of menu @@ -886,6 +893,10 @@ def __init__( model=MenuTreeModelBuilder(filename).GetModel(separators=True), class_handler=self.panel, ) + # Init recent files submenu + self.panel.controller.SetRecentFilesMenu( + menu=self.menubar.GetMenus()[0][0], # Parent File menu + ) self.SetMenuBar(self.menubar) sizer = wx.BoxSizer(wx.VERTICAL) diff --git a/gui/wxpython/gui_core/simplelmgr.py b/gui/wxpython/gui_core/simplelmgr.py index f29a0cab965..38358c4056c 100644 --- a/gui/wxpython/gui_core/simplelmgr.py +++ b/gui/wxpython/gui_core/simplelmgr.py @@ -157,7 +157,7 @@ def OnContextMenu(self, event): _("Copy map names to clipboard (top to bottom)"), _("Copy map names to clipboard (bottom to top)"), ] - for label, text in zip(labels, texts): + for label, text in zip(labels, texts, strict=True): id = NewId() self.Bind( wx.EVT_MENU, diff --git a/gui/wxpython/gui_core/toolbars.py b/gui/wxpython/gui_core/toolbars.py index 753736f60db..939727f2c25 100644 --- a/gui/wxpython/gui_core/toolbars.py +++ b/gui/wxpython/gui_core/toolbars.py @@ -19,6 +19,7 @@ import platform import os from itertools import starmap +from pathlib import Path import wx from wx.lib.agw import aui @@ -281,7 +282,7 @@ def CreateSelectionButton(self, tooltip): Button must be custom (not toolbar tool) to set smaller width. """ arrowPath = os.path.join(IMGDIR, "small_down_arrow.png") - if os.path.isfile(arrowPath) and os.path.getsize(arrowPath): + if Path(arrowPath).is_file() and Path(arrowPath).stat().st_size: bitmap = wx.Bitmap(name=arrowPath) else: bitmap = wx.ArtProvider.GetBitmap( diff --git a/gui/wxpython/gui_core/widgets.py b/gui/wxpython/gui_core/widgets.py index ffbbed02e02..b523f9834e7 100644 --- a/gui/wxpython/gui_core/widgets.py +++ b/gui/wxpython/gui_core/widgets.py @@ -17,6 +17,7 @@ - widgets::EmailValidator - widgets::TimeISOValidator - widgets::MapValidator + - widgets::MapNameValidator - widgets::NTCValidator - widgets::SimpleValidator - widgets::GenericValidator @@ -56,6 +57,7 @@ import re from bisect import bisect from datetime import datetime +from pathlib import Path from core.globalvar import wxPythonPhoenix import wx @@ -65,10 +67,8 @@ from wx.lib.wordwrap import wordwrap if wxPythonPhoenix: - import wx.adv from wx.adv import OwnerDrawnComboBox else: - import wx.combo from wx.combo import OwnerDrawnComboBox try: import wx.lib.agw.flatnotebook as FN @@ -891,6 +891,31 @@ def _mapNameValidationFailed(ctrl): GenericValidator.__init__(self, grass.legal_name, _mapNameValidationFailed) +class MapNameValidator(BaseValidator): + """Validator for map name input + + See G_legal_filename() + """ + + def __init__(self): + BaseValidator.__init__(self) + + def _validate(self, win): + """Validate input""" + text = win.GetValue() + if text: + if not grass.legal_name(text): + self._notvalid() + return False + + self._valid() + return True + + def Clone(self): + """Clone validator""" + return MapNameValidator() + + class GenericMultiValidator(Validator): """This validator checks conditions and calls callbacks in case the condition is not fulfilled. @@ -1556,7 +1581,7 @@ def _loadSettings(self): """ data = {} - if not os.path.exists(self.settingsFile): + if not Path(self.settingsFile).exists(): return data try: @@ -1713,7 +1738,7 @@ def GetPictureBitmap(self, name): return self.bitmaps[name] path = self._getPath(name) - if os.path.exists(path): + if Path(path).exists(): bitmap = wx.Bitmap(path) self.bitmaps[name] = bitmap return bitmap diff --git a/gui/wxpython/iclass/frame.py b/gui/wxpython/iclass/frame.py index c70ca9f6e4d..9da3e40c11d 100644 --- a/gui/wxpython/iclass/frame.py +++ b/gui/wxpython/iclass/frame.py @@ -22,6 +22,7 @@ import os import tempfile from ctypes import byref, pointer +from pathlib import Path import wx @@ -1252,7 +1253,7 @@ def OnSaveSigFile(self, event): dlg = IClassSignatureFileDialog(self, file=self.sigFile) if dlg.ShowModal() == wx.ID_OK: - if os.path.exists(dlg.GetFileName(fullPath=True)): + if Path(dlg.GetFileName(fullPath=True)).exists(): qdlg = wx.MessageDialog( parent=self, message=_( diff --git a/gui/wxpython/iclass/g.gui.iclass.py b/gui/wxpython/iclass/g.gui.iclass.py index abf96a5f404..0d71f9b64f2 100755 --- a/gui/wxpython/iclass/g.gui.iclass.py +++ b/gui/wxpython/iclass/g.gui.iclass.py @@ -108,7 +108,7 @@ def main(): frame = wx.Frame( parent=None, size=globalvar.MAP_WINDOW_SIZE, - title=_("Supervised Classification Tool - GRASS GIS"), + title=_("Supervised Classification Tool - GRASS"), ) frame = IClassMapDisplay( parent=frame, diff --git a/gui/wxpython/iclass/plots.py b/gui/wxpython/iclass/plots.py index 510953bb5b1..ca56eb58ba8 100644 --- a/gui/wxpython/iclass/plots.py +++ b/gui/wxpython/iclass/plots.py @@ -101,7 +101,7 @@ def _createScatterPlotPanel(self): def OnPlotTypeSelected(self, event): """Plot type selected""" - if self.plotSwitch.GetSelection() in [0, 1]: + if self.plotSwitch.GetSelection() in {0, 1}: self.SetupScrolling(scroll_x=False, scroll_y=True) if self.iscatt_panel: self.iscatt_panel.Hide() diff --git a/gui/wxpython/iclass/toolbars.py b/gui/wxpython/iclass/toolbars.py index 4ca67656fb9..ce1c5d61faa 100644 --- a/gui/wxpython/iclass/toolbars.py +++ b/gui/wxpython/iclass/toolbars.py @@ -264,7 +264,7 @@ def _updateColor(self, cat): def SetCategories(self, catNames, catIdx): self.choice.Clear() - for name, idx in zip(catNames, catIdx): + for name, idx in zip(catNames, catIdx, strict=False): self.choice.Append(name, idx) def GetSelectedCategoryName(self): diff --git a/gui/wxpython/icons/icon.py b/gui/wxpython/icons/icon.py index 77ef956278b..99f00e42771 100644 --- a/gui/wxpython/icons/icon.py +++ b/gui/wxpython/icons/icon.py @@ -18,6 +18,7 @@ import os import sys import copy +from pathlib import Path import wx @@ -41,7 +42,7 @@ # join paths try: - if iconPath and not os.path.exists(iconPath): + if iconPath and not Path(iconPath).exists(): raise OSError for key, img in iconSet.items(): @@ -80,7 +81,7 @@ def GetBitmap(self, size=None): id=self.imagepath, client=wx.ART_TOOLBAR, size=size ) elif self.type == "img": - if os.path.isfile(self.imagepath) and os.path.getsize(self.imagepath): + if Path(self.imagepath).is_file() and Path(self.imagepath).stat().st_size: if size and len(size) == 2: image = wx.Image(name=self.imagepath) image.Rescale(size[0], size[1]) diff --git a/gui/wxpython/image2target/g.gui.image2target.html b/gui/wxpython/image2target/g.gui.image2target.html index 35b9c7023dd..926d48e53e3 100644 --- a/gui/wxpython/image2target/g.gui.image2target.html +++ b/gui/wxpython/image2target/g.gui.image2target.html @@ -276,7 +276,7 @@

Toolbar for the GCP list

GCP Map Display Statusbar

The GCP map display statusbar is similar to the statusbar in the regular -GRASS GIS map display with two differences, Go to has been +GRASS map display with two differences, Go to has been replaced with Go to GCP No. and Projection has been replaced with RMS error.

diff --git a/gui/wxpython/image2target/g.gui.image2target.md b/gui/wxpython/image2target/g.gui.image2target.md index e3eda3c2d8b..63d9ff1aeec 100644 --- a/gui/wxpython/image2target/g.gui.image2target.md +++ b/gui/wxpython/image2target/g.gui.image2target.md @@ -237,7 +237,7 @@ imagery group. #### GCP Map Display Statusbar The GCP map display statusbar is similar to the statusbar in the regular -GRASS GIS map display with two differences, *Go to* has been replaced +GRASS map display with two differences, *Go to* has been replaced with *Go to GCP No.* and *Projection* has been replaced with *RMS error*. diff --git a/gui/wxpython/image2target/ii2t_gis_set.py b/gui/wxpython/image2target/ii2t_gis_set.py index e2b3da9e60d..d31d9eaa48e 100644 --- a/gui/wxpython/image2target/ii2t_gis_set.py +++ b/gui/wxpython/image2target/ii2t_gis_set.py @@ -119,7 +119,7 @@ def __init__(self, parent=None, id=wx.ID_ANY, style=wx.DEFAULT_FRAME_STYLE): self.gisdbase_box = StaticBox( parent=self.panel, id=wx.ID_ANY, - label=" %s " % _("1. Select GRASS GIS database directory"), + label=" %s " % _("1. Select GRASS database directory"), ) self.location_box = StaticBox( parent=self.panel, @@ -148,7 +148,7 @@ def __init__(self, parent=None, id=wx.ID_ANY, style=wx.DEFAULT_FRAME_STYLE): self.ldbase = StaticText( parent=self.gisdbase_panel, id=wx.ID_ANY, - label=_("GRASS GIS database directory contains Locations."), + label=_("GRASS database directory contains Locations."), ) self.llocation = StaticWrapText( @@ -296,7 +296,7 @@ def __init__(self, parent=None, id=wx.ID_ANY, style=wx.DEFAULT_FRAME_STYLE): def _set_properties(self, version, revision): """Set frame properties""" - self.SetTitle(_("GRASS GIS %s startup%s") % (version, revision)) + self.SetTitle(_("GRASS %s startup%s") % (version, revision)) self.SetIcon( wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"), wx.BITMAP_TYPE_ICO) ) @@ -313,7 +313,7 @@ def _set_properties(self, version, revision): # set database if not self.gisdbase: # sets an initial path for gisdbase if nothing in GISRC - if os.path.isdir(os.getenv("HOME")): + if Path(os.getenv("HOME")).is_dir(): self.gisdbase = os.getenv("HOME") else: self.gisdbase = str(Path.cwd()) @@ -331,7 +331,7 @@ def _set_properties(self, version, revision): location = self.GetRCValue("LOCATION_NAME") if location == "": return - if not os.path.isdir(os.path.join(self.gisdbase, location)): + if not Path(os.path.join(self.gisdbase, location)).is_dir(): location = None # list of locations @@ -539,8 +539,8 @@ def _readGisRC(self): gisrc = os.getenv("GISRC") - if gisrc and os.path.isfile(gisrc): - with open(gisrc) as rc: + if gisrc and (gisrc_path := Path(gisrc)).is_file(): + with gisrc_path.open() as rc: for line in rc: try: key, val = line.split(":", 1) @@ -920,8 +920,9 @@ def UpdateMapsets(self, location): disabled = [] idx = 0 for mapset in self.listOfMapsets: - if mapset not in self.listOfMapsetsSelectable or os.path.isfile( - os.path.join(self.gisdbase, locationName, mapset, ".gislock") + if ( + mapset not in self.listOfMapsetsSelectable + or Path(self.gisdbase, locationName, mapset, ".gislock").is_file() ): disabled.append(idx) idx += 1 @@ -952,8 +953,9 @@ def OnSelectLocation(self, event): locationName = "" for mapset in self.listOfMapsets: - if mapset not in self.listOfMapsetsSelectable or os.path.isfile( - os.path.join(self.gisdbase, locationName, mapset, ".gislock") + if ( + mapset not in self.listOfMapsetsSelectable + or Path(self.gisdbase, locationName, mapset, ".gislock").is_file() ): disabled.append(idx) idx += 1 @@ -997,7 +999,7 @@ def OnSetDatabase(self, event): """Database set""" gisdbase = self.tgisdbase.GetValue() self._hideMessage() - if not os.path.exists(gisdbase): + if not Path(gisdbase).exists(): self._showError(_("Path '%s' doesn't exist.") % gisdbase) return @@ -1065,7 +1067,7 @@ def CreateNewMapset(self, mapset): try: self.gisdbase = self.tgisdbase.GetValue() location = self.listOfLocations[self.lblocations.GetSelection()] - os.mkdir(os.path.join(self.gisdbase, location, mapset)) + Path(os.path.join(self.gisdbase, location, mapset)).mkdir() # copy WIND file and its permissions from PERMANENT and set # permissions to u+rw,go+r shutil.copy( @@ -1093,7 +1095,7 @@ def OnStart(self, event): mapset = self.listOfMapsets[self.lbmapsets.GetSelection()] lockfile = os.path.join(dbase, location, mapset, ".gislock") - if os.path.isfile(lockfile): + if Path(lockfile).is_file(): dlg = wx.MessageDialog( parent=self, message=_( @@ -1268,7 +1270,7 @@ def OnInit(self): if __name__ == "__main__": if os.getenv("GISBASE") is None: - sys.exit("Failed to start GUI, GRASS GIS is not running.") + sys.exit("Failed to start GUI, GRASS is not running.") GRASSStartUp = StartUp(0) GRASSStartUp.MainLoop() diff --git a/gui/wxpython/image2target/ii2t_manager.py b/gui/wxpython/image2target/ii2t_manager.py index bd2320b37c6..0999cd26bf5 100644 --- a/gui/wxpython/image2target/ii2t_manager.py +++ b/gui/wxpython/image2target/ii2t_manager.py @@ -1,7 +1,7 @@ """ @package gcp.manager -@brief Georectification module for GRASS GIS. Includes ground control +@brief Georectification module for GRASS. Includes ground control point management and interactive point and click GCP creation Classes: @@ -488,10 +488,9 @@ def OnLocation(self, event): tmplist = [p.name for p in Path(self.grassdatabase, self.xylocation).iterdir()] self.mapsetList = [] for item in tmplist: - if os.path.isdir( - os.path.join(self.grassdatabase, self.xylocation, item) - ) and os.path.exists( - os.path.join(self.grassdatabase, self.xylocation, item, "WIND") + if ( + Path(self.grassdatabase, self.xylocation, item).is_dir() + and Path(self.grassdatabase, self.xylocation, item, "WIND").exists() ): if item != "PERMANENT": self.mapsetList.append(item) @@ -1088,7 +1087,7 @@ def __init__( } # make a backup of the current points file - if os.path.exists(self.file["control_points"]): + if Path(self.file["control_points"]).exists(): shutil.copy(self.file["control_points"], self.file["control_points_bak"]) # polynomial order transformation for georectification @@ -1449,7 +1448,7 @@ def SetGCPData(self, coordtype, coord, mapdisp=None, confirm=False): self.mapcoordlist[key][4] = coord[0] self.mapcoordlist[key][5] = coord[1] # ADD ELEVATION FROM MAP AS HEIGHT PARAMETER - if os.path.exists(self.file["elevation"]): + if Path(self.file["elevation"]).exists(): # Parse the i.ortho.elev generated file # Get all lines from file lines = Path(self.file["elevation"]).read_text().splitlines() @@ -1870,12 +1869,12 @@ def OnQuit(self, event): self.SaveGCPs(None) elif ret == wx.NO: # restore POINTS file from backup - if os.path.exists(self.file["control_points_bak"]): + if Path(self.file["control_points_bak"]).exists(): shutil.copy( self.file["control_points_bak"], self.file["control_points"] ) - if os.path.exists(self.file["control_points_bak"]): + if Path(self.file["control_points_bak"]).exists(): os.unlink(self.file["control_points_bak"]) self.SrcMap.Clean() @@ -2357,7 +2356,7 @@ def LoadData(self): self.DeleteAllItems() self.render = False - if os.path.isfile(self.gcp.file["control_points"]): + if Path(self.gcp.file["control_points"]).is_file(): self.gcp.ReadGCPs() else: # 3 gcp is minimum @@ -2560,7 +2559,7 @@ def __init__( ).iterdir() ] for dir in vectlist: - if not os.path.isfile( + if not Path( os.path.join( self.grassdatabase, self.xylocation, @@ -2569,7 +2568,7 @@ def __init__( dir, "coor", ) - ): + ).is_file(): vectlist.remove(dir) utils.ListSortLower(vectlist) @@ -2596,7 +2595,7 @@ def __init__( # self.listMap = CheckListBox(parent=self, id=wx.ID_ANY, choices=vectlist) - if os.path.isfile(self.vgrpfile): + if Path(self.vgrpfile).is_file(): with open(self.vgrpfile) as f: checked = [] for line in f: diff --git a/gui/wxpython/image2target/ii2t_mapdisplay.py b/gui/wxpython/image2target/ii2t_mapdisplay.py index 9ee3b77d118..d7bc7c8c9b9 100644 --- a/gui/wxpython/image2target/ii2t_mapdisplay.py +++ b/gui/wxpython/image2target/ii2t_mapdisplay.py @@ -47,7 +47,7 @@ def __init__( self, parent, giface, - title=_("GRASS GIS Manage Ground Control Points"), + title=_("GRASS Manage Ground Control Points"), toolbars=["gcpdisp"], Map=None, auimgr=None, diff --git a/gui/wxpython/iscatt/controllers.py b/gui/wxpython/iscatt/controllers.py index 873ddf02071..c3438a72cf1 100644 --- a/gui/wxpython/iscatt/controllers.py +++ b/gui/wxpython/iscatt/controllers.py @@ -151,7 +151,7 @@ def SetBands(self, bands): show_add = bool(self.show_add_scatt_plot) - self.all_bands_to_bands = dict(zip(bands, [-1] * len(bands))) + self.all_bands_to_bands = dict(zip(bands, [-1] * len(bands), strict=False)) self.all_bands = bands self.region = GetRegion() diff --git a/gui/wxpython/iscatt/plots.py b/gui/wxpython/iscatt/plots.py index 2f9354b3202..2fbac07cfbd 100644 --- a/gui/wxpython/iscatt/plots.py +++ b/gui/wxpython/iscatt/plots.py @@ -111,13 +111,13 @@ def _createWidgets(self): self.axes = self.fig.add_axes([0.0, 0.0, 1, 1]) - pol = Polygon(list(zip([0], [0])), animated=True) + pol = Polygon(list(zip([0], [0], strict=False)), animated=True) self.axes.add_patch(pol) self.polygon_drawer = PolygonDrawer(self.axes, pol=pol, empty_pol=True) self.zoom_wheel_coords = None self.zoom_rect_coords = None - self.zoom_rect = Polygon(list(zip([0], [0])), facecolor="none") + self.zoom_rect = Polygon(list(zip([0], [0], strict=False)), facecolor="none") self.zoom_rect.set_visible(False) self.axes.add_patch(self.zoom_rect) @@ -645,7 +645,7 @@ def __init__(self, ax, pol, empty_pol): self.pol = pol self.empty_pol = empty_pol - x, y = zip(*self.pol.xy) + x, y = zip(*self.pol.xy, strict=False) self.line = Line2D(x, y, marker="o", markerfacecolor="r", animated=True) self.ax.add_line(self.line) @@ -816,7 +816,7 @@ def _deleteVertex(self, event): coords.append(tup) self.pol.xy = coords - self.line.set_data(list(zip(*self.pol.xy))) + self.line.set_data(list(zip(*self.pol.xy, strict=False))) self.Redraw() @@ -837,7 +837,7 @@ def _addVertexOnBoundary(self, event): + [(event.xdata, event.ydata)] + list(self.pol.xy[i + 1 :]) ) - self.line.set_data(list(zip(*self.pol.xy))) + self.line.set_data(list(zip(*self.pol.xy, strict=False))) break self.Redraw() @@ -855,7 +855,7 @@ def _addVertex(self, event): + [(event.xdata, event.ydata)] ) - self.line.set_data(list(zip(*self.pol.xy))) + self.line.set_data(list(zip(*self.pol.xy, strict=False))) self.Redraw() @@ -884,7 +884,7 @@ def motion_notify_callback(self, event): elif self.moving_ver_idx == len(self.pol.xy) - 1: self.pol.xy[0] = x, y - self.line.set_data(list(zip(*self.pol.xy))) + self.line.set_data(list(zip(*self.pol.xy, strict=False))) self.canvas.restore_region(self.background) diff --git a/gui/wxpython/lmgr/frame.py b/gui/wxpython/lmgr/frame.py index 9524c3a8b5d..1ab5692d980 100644 --- a/gui/wxpython/lmgr/frame.py +++ b/gui/wxpython/lmgr/frame.py @@ -89,8 +89,8 @@ class GMFrame(wx.Frame): - """Layer Manager frame with notebook widget for controlling GRASS - GIS. Includes command console page for typing GRASS (and other) + """Layer Manager frame with notebook widget for controlling GRASS. + Includes command console page for typing GRASS (and other) commands, tree widget page for managing map layers. """ @@ -108,7 +108,7 @@ def __init__( if title: self.baseTitle = title else: - self.baseTitle = _("GRASS GIS") + self.baseTitle = _("GRASS") self.iconsize = (16, 16) @@ -1223,7 +1223,7 @@ def OnRunScript(self, event): if not filename: return False - if not os.path.exists(filename): + if not Path(filename).exists(): GError( parent=self, message=_("Script file '%s' doesn't exist. Operation canceled.") @@ -1462,8 +1462,9 @@ def OnSystemInfo(self, event): sys.stderr.write(_("Unable to get GRASS version\n")) # check also OSGeo4W on MS Windows - if sys.platform == "win32" and not os.path.exists( - os.path.join(os.getenv("GISBASE"), "WinGRASS-README.url") + if ( + sys.platform == "win32" + and not Path(os.getenv("GISBASE"), "WinGRASS-README.url").exists() ): osgeo4w = " (OSGeo4W)" else: diff --git a/gui/wxpython/lmgr/workspace.py b/gui/wxpython/lmgr/workspace.py index 395fd0b61a5..2d46d0d0943 100644 --- a/gui/wxpython/lmgr/workspace.py +++ b/gui/wxpython/lmgr/workspace.py @@ -21,7 +21,6 @@ from pathlib import Path import wx -import wx.aui from core.settings import UserSettings from core.gcmd import RunCommand, GError, GMessage @@ -209,7 +208,7 @@ def Load(self, filename): self.lmgr.SetSize(gxwXml.layerManager["size"]) if gxwXml.layerManager["cwd"]: self.lmgr.cwdPath = gxwXml.layerManager["cwd"] - if os.path.isdir(self.lmgr.cwdPath): + if Path(self.lmgr.cwdPath).is_dir(): os.chdir(self.lmgr.cwdPath) # @@ -383,7 +382,7 @@ def SaveAs(self): if filename[-4:] != ".gxw": filename += ".gxw" - if os.path.exists(filename): + if Path(filename).exists(): dlg = wx.MessageDialog( self.lmgr, message=_( diff --git a/gui/wxpython/location_wizard/wizard.py b/gui/wxpython/location_wizard/wizard.py index 331029b4ede..cb1db881150 100644 --- a/gui/wxpython/location_wizard/wizard.py +++ b/gui/wxpython/location_wizard/wizard.py @@ -1479,7 +1479,7 @@ def OnEnterPage(self, event: WizardEvent) -> None: event.Skip() def OnPageChanging(self, event: WizardEvent) -> None: - if event.GetDirection() and not os.path.isfile(self.georeffile): + if event.GetDirection() and not Path(self.georeffile).is_file(): event.Veto() self.GetNext().SetPrev(self) @@ -1487,7 +1487,7 @@ def OnText(self, event): """File changed""" self.georeffile = event.GetString() nextButton = wx.FindWindowById(wx.ID_FORWARD) - if len(self.georeffile) > 0 and os.path.isfile(self.georeffile): + if len(self.georeffile) > 0 and Path(self.georeffile).is_file(): if not nextButton.IsEnabled(): nextButton.Enable(True) elif nextButton.IsEnabled(): @@ -2644,7 +2644,7 @@ def OnWizFinished(self): location = self.startpage.location # location already exists? - if os.path.isdir(os.path.join(database, location)): + if Path(database, location).is_dir(): GError( parent=self.wizard, message="%s <%s>: %s" @@ -2660,10 +2660,10 @@ def OnWizFinished(self): current_gdb = decode(grass.gisenv()["GISDBASE"]) if current_gdb != database: # change to new GISDbase or create new one - if not os.path.isdir(database): + if not Path(database).is_dir(): # create new directory try: - os.mkdir(database) + Path(database).mkdir() except OSError as error: GError( parent=self.wizard, @@ -2730,8 +2730,9 @@ def OnWizFinished(self): desc=self.startpage.locTitle, ) elif coordsys == "file": - if not self.filepage.georeffile or not os.path.isfile( - self.filepage.georeffile + if ( + not self.filepage.georeffile + or not Path(self.filepage.georeffile).is_file() ): return _("File <%s> not found.") % self.filepage.georeffile diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index f6ab655d6c0..ebcb420dbd5 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -125,7 +125,7 @@ def __init__( if title: self.baseTitle = title else: - self.baseTitle = _("GRASS GIS") + self.baseTitle = _("GRASS") self.iconsize = (16, 16) self.size = size @@ -1379,7 +1379,7 @@ def OnRunScript(self, event): if not filename: return False - if not os.path.exists(filename): + if not Path(filename).exists(): GError( parent=self, message=_("Script file '%s' doesn't exist. Operation canceled.") @@ -1618,8 +1618,9 @@ def OnSystemInfo(self, event): sys.stderr.write(_("Unable to get GRASS version\n")) # check also OSGeo4W on MS Windows - if sys.platform == "win32" and not os.path.exists( - os.path.join(os.getenv("GISBASE"), "WinGRASS-README.url") + if ( + sys.platform == "win32" + and not Path(os.getenv("GISBASE"), "WinGRASS-README.url").exists() ): osgeo4w = " (OSGeo4W)" else: @@ -1710,7 +1711,8 @@ def OnWorkspaceSaveAs(self, event=None): def OnWorkspaceClose(self, event=None): """Close file with workspace definition""" - self.workspace_manager.Close() + if self.workspace_manager.CanClosePage(caption=_("Close workspace")): + self.workspace_manager.Close() def OnDisplayClose(self, event=None): """Close current map display window""" diff --git a/gui/wxpython/mapdisp/main.py b/gui/wxpython/mapdisp/main.py index 5554338e8e7..18d654838eb 100644 --- a/gui/wxpython/mapdisp/main.py +++ b/gui/wxpython/mapdisp/main.py @@ -32,6 +32,7 @@ import time import shutil import fileinput +from pathlib import Path from grass.script.utils import try_remove from grass.script import core as grass @@ -614,9 +615,9 @@ def watcher(self): # self.timer.Stop() # return - # todo: events + # TODO: events try: - currentCmdFileTime = os.path.getmtime(monFile["cmd"]) + currentCmdFileTime = Path(monFile["cmd"]).stat().st_mtime if currentCmdFileTime > self.cmdTimeStamp: self.timer.Stop() self.cmdTimeStamp = currentCmdFileTime diff --git a/gui/wxpython/mapswipe/g.gui.mapswipe.py b/gui/wxpython/mapswipe/g.gui.mapswipe.py index d4f19c0244a..51474170520 100755 --- a/gui/wxpython/mapswipe/g.gui.mapswipe.py +++ b/gui/wxpython/mapswipe/g.gui.mapswipe.py @@ -84,7 +84,7 @@ def main(): frame = wx.Frame( parent=None, size=globalvar.MAP_WINDOW_SIZE, - title=_("Map Swipe Tool - GRASS GIS"), + title=_("Map Swipe Tool - GRASS"), ) frame = SwipeMapDisplay( parent=frame, diff --git a/gui/wxpython/mapswipe/toolbars.py b/gui/wxpython/mapswipe/toolbars.py index 4788578987b..325ab0c32e1 100644 --- a/gui/wxpython/mapswipe/toolbars.py +++ b/gui/wxpython/mapswipe/toolbars.py @@ -137,7 +137,7 @@ def __init__(self, parent): # add tool to toggle active map window self.toggleMode = wx.Choice(parent=self) for label, cdata in zip( - [_("Swipe mode"), _("Mirror mode")], ["swipe", "mirror"] + [_("Swipe mode"), _("Mirror mode")], ["swipe", "mirror"], strict=True ): self.toggleMode.Append(label, cdata) self.toggleMode.SetSelection(0) diff --git a/gui/wxpython/mapwin/buffered.py b/gui/wxpython/mapwin/buffered.py index 9488f72e54b..b24fffc721b 100644 --- a/gui/wxpython/mapwin/buffered.py +++ b/gui/wxpython/mapwin/buffered.py @@ -25,6 +25,7 @@ import time import math import sys +from pathlib import Path import wx @@ -798,8 +799,8 @@ def GetOverlay(self): for overlay in self.Map.GetListOfLayers(ltype="overlay", active=True): if ( overlay.mapfile is None - or not os.path.isfile(overlay.mapfile) - or (not os.path.getsize(overlay.mapfile)) + or not Path(overlay.mapfile).is_file() + or (not Path(overlay.mapfile).stat().st_size) ): continue img = utils.autoCropImageFromFile(overlay.mapfile) @@ -823,8 +824,8 @@ def GetImage(self): imgId = 99 if ( self.Map.mapfile - and os.path.isfile(self.Map.mapfile) - and os.path.getsize(self.Map.mapfile) + and Path(self.Map.mapfile).is_file() + and Path(self.Map.mapfile).stat().st_size ): img = wx.Image(self.Map.mapfile, wx.BITMAP_TYPE_ANY) else: @@ -1117,7 +1118,7 @@ def DragMap(self, moveto): def DragItem(self, id, coords): """Drag an overlay decoration item""" - if id in (99, "") or id is None: + if id in {99, ""} or id is None: return Debug.msg(5, "BufferedWindow.DragItem(): id=%d" % id) x, y = self.lastpos diff --git a/gui/wxpython/modules/colorrules.py b/gui/wxpython/modules/colorrules.py index 61b855e4c90..d505b6fbd44 100644 --- a/gui/wxpython/modules/colorrules.py +++ b/gui/wxpython/modules/colorrules.py @@ -284,7 +284,7 @@ def Enable(self, enable=True): for child in self.mainPanel.GetChildren(): child.Enable(enable) sql = True - self.LoadRulesline(sql) # todo + self.LoadRulesline(sql) # TODO self.btnAdd.Enable(enable) self.numRules.Enable(enable) self.checkAll.Enable(enable) @@ -665,7 +665,7 @@ def OnSaveRulesFile(self, event): if not path: return - if os.path.exists(path): + if Path(path).exists(): dlgOw = wx.MessageDialog( self, message=_( @@ -692,7 +692,7 @@ def OnSaveRulesFile(self, event): def OnLoadRulesFile(self, event): """Load color table from file""" path = event.GetString() - if not os.path.exists(path): + if not Path(path).exists(): return self.rulesPanel.Clear() @@ -2050,8 +2050,8 @@ def GetImage(self): """Converts files to wx.Image""" if ( self.Map.mapfile - and os.path.isfile(self.Map.mapfile) - and os.path.getsize(self.Map.mapfile) + and Path(self.Map.mapfile).is_file() + and Path(self.Map.mapfile).stat().st_size ): img = wx.Image(self.Map.mapfile, wx.BITMAP_TYPE_ANY) else: diff --git a/gui/wxpython/modules/histogram.py b/gui/wxpython/modules/histogram.py index 61d41809858..a5dec29d0e7 100644 --- a/gui/wxpython/modules/histogram.py +++ b/gui/wxpython/modules/histogram.py @@ -19,6 +19,7 @@ import os import sys +from pathlib import Path import wx @@ -212,8 +213,8 @@ def GetImage(self): """Converts files to wx.Image""" if ( self.Map.mapfile - and os.path.isfile(self.Map.mapfile) - and os.path.getsize(self.Map.mapfile) + and Path(self.Map.mapfile).is_file() + and Path(self.Map.mapfile).stat().st_size ): img = wx.Image(self.Map.mapfile, wx.BITMAP_TYPE_ANY) else: diff --git a/gui/wxpython/modules/import_export.py b/gui/wxpython/modules/import_export.py index 2339a344048..0ba706191b6 100644 --- a/gui/wxpython/modules/import_export.py +++ b/gui/wxpython/modules/import_export.py @@ -494,9 +494,11 @@ def OnRun(self, event): idsn = dsn if "PG:" in dsn: idsn = f"{dsn} table={layer}" - elif os.path.exists(idsn): + elif Path(idsn).exists(): try: from osgeo import gdal + + gdal.DontUseExceptions() except ImportError: GError( parent=self, diff --git a/gui/wxpython/nviz/mapwindow.py b/gui/wxpython/nviz/mapwindow.py index 1559da45360..7e650ae3870 100644 --- a/gui/wxpython/nviz/mapwindow.py +++ b/gui/wxpython/nviz/mapwindow.py @@ -1171,7 +1171,7 @@ def ChangeInnerView(self): view["position"]["y"], iview["height"]["value"], ) = self._display.GetViewpointPosition() - for key, val in zip(("x", "y", "z"), self._display.GetViewdir()): + for key, val in zip(("x", "y", "z"), self._display.GetViewdir(), strict=False): iview["dir"][key] = val iview["dir"]["use"] = True diff --git a/gui/wxpython/nviz/tools.py b/gui/wxpython/nviz/tools.py index e398000ac00..c0e10b6e197 100644 --- a/gui/wxpython/nviz/tools.py +++ b/gui/wxpython/nviz/tools.py @@ -19,7 +19,6 @@ @author Anna Kratochvilova (Google SoC 2011) """ -import os import sys import copy @@ -331,7 +330,7 @@ def _createViewPage(self): # perspective # set initial defaults here (or perhaps in a default values file), not in user # settings - # todo: consider setting an absolute max at 360 instead of undefined. + # TODO: consider setting an absolute max at 360 instead of undefined. # (leave the default max value at pi) tooltip = _( "Adjusts the distance and angular perspective of the image viewpoint" @@ -2783,7 +2782,7 @@ def OnSaveAnimation(self, event): if not prefix: GMessage(parent=self, message=_("No file prefix given.")) return - if not os.path.exists(dir): + if not Path(dir).exists(): GMessage(parent=self, message=_("Directory %s does not exist.") % dir) return @@ -3222,7 +3221,9 @@ def _createCompass(self, panel, sizer, type): padding = 20 minWidth = sw.GetTextExtent(sw.GetLabel())[0] + padding for win, name in zip( - (w, n, s, e, nw, ne, se, sw), ("w", "n", "s", "e", "nw", "ne", "se", "sw") + (w, n, s, e, nw, ne, se, sw), + ("w", "n", "s", "e", "nw", "ne", "se", "sw"), + strict=True, ): win.SetMinSize((minWidth, -1)) win.Bind(wx.EVT_BUTTON, self.OnLookFrom) @@ -4849,7 +4850,7 @@ def OnSliceReset(self, event): return for coord, val in zip( - ("x1", "x2", "y1", "y2", "z1", "z2"), (0, 1, 0, 1, 0, 1, 0) + ("x1", "x2", "y1", "y2", "z1", "z2"), (0, 1, 0, 1, 0, 1, 0), strict=True ): data["volume"]["slice"][sel]["position"][coord] = val data["volume"]["slice"][sel]["position"]["update"] = None diff --git a/gui/wxpython/photo2image/g.gui.photo2image.html b/gui/wxpython/photo2image/g.gui.photo2image.html index af113100511..ec12456b016 100644 --- a/gui/wxpython/photo2image/g.gui.photo2image.html +++ b/gui/wxpython/photo2image/g.gui.photo2image.html @@ -2,7 +2,7 @@

DESCRIPTION

-This module is based on g.gui.gcp, the GCP manager of GRASS GIS. +This module is based on g.gui.gcp, the GCP manager of GRASS. It is part of i.ortho.photo suite.

diff --git a/gui/wxpython/photo2image/g.gui.photo2image.md b/gui/wxpython/photo2image/g.gui.photo2image.md index da1ab646ee3..57ee9df99ca 100644 --- a/gui/wxpython/photo2image/g.gui.photo2image.md +++ b/gui/wxpython/photo2image/g.gui.photo2image.md @@ -7,7 +7,7 @@ index: topic_GUI|GUI ## DESCRIPTION -This module is based on **g.gui.gcp**, the GCP manager of GRASS GIS. It +This module is based on **g.gui.gcp**, the GCP manager of GRASS. It is part of i.ortho.photo suite. The aim of this module is to give absolute location values to the diff --git a/gui/wxpython/photo2image/ip2i_manager.py b/gui/wxpython/photo2image/ip2i_manager.py index beb2cffce9d..84a0f15835c 100644 --- a/gui/wxpython/photo2image/ip2i_manager.py +++ b/gui/wxpython/photo2image/ip2i_manager.py @@ -1,7 +1,7 @@ """ @package photo2image.ip2i_manager -@brief Scanning distortion correction of a photo for GRASS GIS. +@brief Scanning distortion correction of a photo for GRASS. Includes ground control point management and interactive point and click GCP creation @@ -30,6 +30,7 @@ import sys import shutil from copy import copy +from pathlib import Path import wx from wx.lib.mixins.listctrl import ColumnSorterMixin, ListCtrlAutoWidthMixin @@ -407,7 +408,7 @@ def __init__( } # make a backup of the current points file if exists - if os.path.exists(self.file["points"]): + if Path(self.file["points"]).exists(): shutil.copy(self.file["points"], self.file["points_bak"]) shutil.copy(self.file["points"], self.file["ref_points"]) GMessage(_("A POINTS file exists, renaming it to POINTS_BAK")) @@ -1187,11 +1188,11 @@ def OnQuit(self, event): self.SaveGCPs(None) elif ret == wx.NO: # restore POINTS file from backup - if os.path.exists(self.file["points_bak"]): + if Path(self.file["points_bak"]).exists(): shutil.copy(self.file["points_bak"], self.file["points"]) shutil.copy(self.file["points_bak"], self.file["ref_points"]) - if os.path.exists(self.file["points_bak"]): + if Path(self.file["points_bak"]).exists(): os.unlink(self.file["points_bak"]) self.SrcMap.Clean() @@ -1669,7 +1670,7 @@ def LoadData(self): self.DeleteAllItems() self.render = False - if os.path.isfile(self.gcp.file["points"]): + if Path(self.gcp.file["points"]).is_file(): self.gcp.ReadGCPs() else: # 3 gcp is minimum diff --git a/gui/wxpython/photo2image/ip2i_mapdisplay.py b/gui/wxpython/photo2image/ip2i_mapdisplay.py index 732716a1a0c..aabc86e5e39 100644 --- a/gui/wxpython/photo2image/ip2i_mapdisplay.py +++ b/gui/wxpython/photo2image/ip2i_mapdisplay.py @@ -42,7 +42,7 @@ def __init__( self, parent, giface, - title=_("GRASS GIS Manage Location of Tick Points on a Scanned Photo"), + title=_("GRASS Manage Location of Tick Points on a Scanned Photo"), toolbars=["gcpdisp"], Map=None, auimgr=None, diff --git a/gui/wxpython/psmap/dialogs.py b/gui/wxpython/psmap/dialogs.py index 2561e3c388e..3245d9bc107 100644 --- a/gui/wxpython/psmap/dialogs.py +++ b/gui/wxpython/psmap/dialogs.py @@ -644,7 +644,7 @@ def __init__(self, parent, id, settings, env): _("Top"), _("Bottom"), ] - self.catsLabels = dict(zip(self.cat, labels)) + self.catsLabels = dict(zip(self.cat, labels, strict=True)) paperString = RunCommand("ps.map", flags="p", read=True, quiet=True) self.paperTable = self._toList(paperString) self.unitsList = self.unitConv.getPageUnitsNames() @@ -820,7 +820,7 @@ def getCtrl(self, item): def _toList(self, paperStr): sizeList = [] for line in paperStr.strip().split("\n"): - d = dict(zip([self.cat[1]] + self.cat[3:], line.split())) + d = dict(zip([self.cat[1]] + self.cat[3:], line.split(), strict=False)) sizeList.append(d) d = {}.fromkeys([self.cat[1]] + self.cat[3:], 100) d.update(Format=_("custom")) @@ -4334,7 +4334,7 @@ def updateVectorLegend(self): vList[i][3] = self.vectorListCtrl.GetItemData(item) vList[i][4] = self.vectorListCtrl.GetItem(item, 1).GetText() vmaps = self.instruction.FindInstructionByType("vProperties", list=True) - for vmap, vector in zip(vmaps, vList): + for vmap, vector in zip(vmaps, vList, strict=False): self.instruction[vmap.id]["lpos"] = vector[3] self.instruction[vmap.id]["label"] = vector[4] # units @@ -4971,8 +4971,8 @@ def _scalebarPanel(self): os.path.join(globalvar.IMGDIR, "scalebar-fancy.png"), os.path.join(globalvar.IMGDIR, "scalebar-simple.png"), ) - for item, path in zip(["fancy", "simple"], imagePath): - bitmap = EmptyBitmap(0, 0) if not os.path.exists(path) else wx.Bitmap(path) + for item, path in zip(["fancy", "simple"], imagePath, strict=True): + bitmap = EmptyBitmap(0, 0) if not Path(path).exists() else wx.Bitmap(path) self.sbCombo.Append(item="", bitmap=bitmap, clientData=item[0]) # self.sbCombo.Append( # item="simple", @@ -5988,7 +5988,7 @@ def OnImageSelectionChanged(self, event): return basePath = self.imagePanel.image["dir"].GetValue() file = os.path.join(basePath, imageName) - if not os.path.exists(file): + if not Path(file).exists(): return if os.path.splitext(file)[1].lower() == ".eps": diff --git a/gui/wxpython/psmap/g.gui.psmap.py b/gui/wxpython/psmap/g.gui.psmap.py index 7e855d5e143..e80a9e606f8 100755 --- a/gui/wxpython/psmap/g.gui.psmap.py +++ b/gui/wxpython/psmap/g.gui.psmap.py @@ -49,7 +49,7 @@ def main(): app = wx.App() frame = PsMapFrame( parent=None, - title=_("Cartographic Composer - GRASS GIS"), + title=_("Cartographic Composer - GRASS"), ) frame.Show() diff --git a/gui/wxpython/psmap/instructions.py b/gui/wxpython/psmap/instructions.py index f526b039884..635a2d96f53 100644 --- a/gui/wxpython/psmap/instructions.py +++ b/gui/wxpython/psmap/instructions.py @@ -817,7 +817,11 @@ def Read(self, instruction, text, **kwargs): instr = {} self.cats = ["Width", "Height", "Left", "Right", "Top", "Bottom"] self.subInstr = dict( - zip(["width", "height", "left", "right", "top", "bottom"], self.cats) + zip( + ["width", "height", "left", "right", "top", "bottom"], + self.cats, + strict=True, + ) ) if instruction == "paper": # just for sure @@ -866,7 +870,7 @@ def _toDict(self, paperStr): sizeDict = {} # cats = self.subInstr[ 'Width', 'Height', 'Left', 'Right', 'Top', 'Bottom'] for line in paperStr.strip().split("\n"): - d = dict(zip(self.cats, line.split()[1:])) + d = dict(zip(self.cats, line.split()[1:], strict=False)) sizeDict[line.split()[0]] = d return sizeDict @@ -1131,7 +1135,7 @@ def Read(self, instruction, text, **kwargs): except (IndexError, ValueError): GError(_("Failed to read instruction %s") % instruction) return False - if not os.path.exists(instr["epsfile"]): + if not Path(instr["epsfile"]).exists(): GError( _("Failed to read instruction %(inst)s: file %(file)s not found.") % {"inst": instruction, "file": instr["epsfile"]} diff --git a/gui/wxpython/rdigit/g.gui.rdigit.py b/gui/wxpython/rdigit/g.gui.rdigit.py index 55d1b16760d..8c26eaa05b3 100755 --- a/gui/wxpython/rdigit/g.gui.rdigit.py +++ b/gui/wxpython/rdigit/g.gui.rdigit.py @@ -230,7 +230,7 @@ def OnMapCreated(self, name, ltype, add: bool | None = None): id=wx.ID_ANY, size=(850, 600), style=wx.DEFAULT_FRAME_STYLE, - title=_("Raster Digitizer - GRASS GIS"), + title=_("Raster Digitizer - GRASS"), ) frame = RDigitMapDisplay(parent=frame, **kwargs) frame.Show() diff --git a/gui/wxpython/rlisetup/functions.py b/gui/wxpython/rlisetup/functions.py index 6a6eec30515..1c7026b1901 100644 --- a/gui/wxpython/rlisetup/functions.py +++ b/gui/wxpython/rlisetup/functions.py @@ -7,6 +7,7 @@ import wx import os import sys +from pathlib import Path from grass.script import core as grass from core.gcmd import GError @@ -58,9 +59,9 @@ def retRLiPath(): grass_config_dir = os.path.join(os.getenv("HOME"), grass_config_dirname) rlipath = os.path.join(grass_config_dir, "r.li") - if os.path.exists(rlipath): + if Path(rlipath).exists(): return rlipath - os.mkdir(rlipath) + Path(rlipath).mkdir() return rlipath diff --git a/gui/wxpython/rlisetup/g.gui.rlisetup.py b/gui/wxpython/rlisetup/g.gui.rlisetup.py index bed4b030f04..8876e1ea5eb 100644 --- a/gui/wxpython/rlisetup/g.gui.rlisetup.py +++ b/gui/wxpython/rlisetup/g.gui.rlisetup.py @@ -45,7 +45,7 @@ def main(): frame = RLiSetupFrame( parent=None, giface=StandaloneGrassInterface(), - title=_("Setup for r.li modules - GRASS GIS"), + title=_("Setup for r.li modules - GRASS"), ) frame.Show() frame.CenterOnScreen() diff --git a/gui/wxpython/startup/guiutils.py b/gui/wxpython/startup/guiutils.py index 135714d779f..de390ec5257 100644 --- a/gui/wxpython/startup/guiutils.py +++ b/gui/wxpython/startup/guiutils.py @@ -1,7 +1,7 @@ """ @package startup.guiutils -@brief General GUI-dependent utilities for GUI startup of GRASS GIS +@brief General GUI-dependent utilities for GUI startup of GRASS (C) 2018 by Vaclav Petras the GRASS Development Team diff --git a/gui/wxpython/startup/locdownload.py b/gui/wxpython/startup/locdownload.py index 7cdf89a553f..a9383f53dc8 100644 --- a/gui/wxpython/startup/locdownload.py +++ b/gui/wxpython/startup/locdownload.py @@ -22,6 +22,8 @@ import textwrap import time +from pathlib import Path + import wx from wx.lib.newevent import NewEvent @@ -72,7 +74,7 @@ }, { "label": "GISMentors dataset, Czech Republic", - "url": "http://training.gismentors.eu/geodata/grass/gismentors.zip", + "url": "https://www.training.gismentors.cz/geodata/grass/gismentors.zip", }, { "label": "Natural Earth Dataset in WGS84", @@ -322,7 +324,7 @@ def DownloadItem(self, item): url = item["url"] dirname = location_name_from_url(url) destination = os.path.join(self.database, dirname) - if os.path.exists(destination): + if Path(destination).exists(): self._error( _( "Project name {name} already exists in {path}, download canceled" @@ -379,7 +381,7 @@ def CheckItem(self, item): url = item["url"] dirname = location_name_from_url(url) destination = os.path.join(self.database, dirname) - if os.path.exists(destination): + if Path(destination).exists(): self._warning( _("Project named {name} already exists, rename it first").format( name=dirname diff --git a/gui/wxpython/timeline/frame.py b/gui/wxpython/timeline/frame.py index e1004ce35d4..7d58c37c60c 100644 --- a/gui/wxpython/timeline/frame.py +++ b/gui/wxpython/timeline/frame.py @@ -336,9 +336,9 @@ def _draw2dFigure(self): # TODO: mixed if mapType == "interval": end = convert(self.timeData[name]["end_datetime"]) - lookUpData = list(zip(start, end)) + lookUpData = list(zip(start, end, strict=False)) duration = end - np.array(start) - barData = list(zip(start, duration)) + barData = list(zip(start, duration, strict=False)) lookUp.AddDataset( type_="bar", yrange=(i - 0.1, i + 0.1), diff --git a/gui/wxpython/timeline/g.gui.timeline.py b/gui/wxpython/timeline/g.gui.timeline.py index 2b2afc2b01b..eaf9c8a776f 100755 --- a/gui/wxpython/timeline/g.gui.timeline.py +++ b/gui/wxpython/timeline/g.gui.timeline.py @@ -61,7 +61,7 @@ def main(): app = wx.App() frame = TimelineFrame( parent=None, - title=_("Timeline Tool - GRASS GIS"), + title=_("Timeline Tool - GRASS"), ) frame.SetDatasets(datasets) frame.Show3D(view3d) diff --git a/gui/wxpython/tools/update_menudata.py b/gui/wxpython/tools/update_menudata.py index eab20519182..962f452e47b 100644 --- a/gui/wxpython/tools/update_menudata.py +++ b/gui/wxpython/tools/update_menudata.py @@ -164,6 +164,6 @@ def main(argv=None): if __name__ == "__main__": if os.getenv("GISBASE") is None: - sys.exit("You must be in GRASS GIS to run this program.") + sys.exit("You must be in GRASS to run this program.") sys.exit(main()) diff --git a/gui/wxpython/tplot/frame.py b/gui/wxpython/tplot/frame.py index adaed601ebc..506e1b50db6 100755 --- a/gui/wxpython/tplot/frame.py +++ b/gui/wxpython/tplot/frame.py @@ -19,7 +19,6 @@ @author start stvds support Matej Krejci """ -import os from itertools import cycle from pathlib import Path import numpy as np @@ -751,7 +750,11 @@ def _writeCSV(self, x, y): """Used to write CSV file of plotted data""" import csv - zipped = list(zip(x, *y)) if isinstance(y[0], list) else list(zip(x, y)) + zipped = ( + list(zip(x, *y, strict=False)) + if isinstance(y[0], list) + else list(zip(x, y, strict=False)) + ) with open(self.csvpath, "w", newline="") as fi: writer = csv.writer(fi) if self.header: @@ -997,7 +1000,7 @@ def OnRedraw(self, event=None): self.init() self.csvpath = self.csvButton.GetValue() self.header = self.headerCheck.IsChecked() - if os.path.exists(self.csvpath) and not self.overwrite: + if Path(self.csvpath).exists() and not self.overwrite: dlg = wx.MessageDialog( self, _("{pa} already exists, do you want to overwrite?").format( @@ -1460,7 +1463,7 @@ def __call__(self, event): x = xData[np.argmin(abs(xData - x))] info = self.lookUp.GetInformation(x) - ys = list(zip(*info[1].values()))[1] + ys = list(zip(*info[1].values(), strict=False))[1] if not info: return # Update the annotation in the current axis.. diff --git a/gui/wxpython/tplot/g.gui.tplot.py b/gui/wxpython/tplot/g.gui.tplot.py index b95a6be2d02..c5c5aac1165 100755 --- a/gui/wxpython/tplot/g.gui.tplot.py +++ b/gui/wxpython/tplot/g.gui.tplot.py @@ -170,7 +170,7 @@ def main(): frame = TplotFrame( parent=None, giface=StandaloneGrassInterface(), - title=_("Temporal Plot Tool - GRASS GIS"), + title=_("Temporal Plot Tool - GRASS"), ) if flags["l"]: frame.linRegRaster.SetValue(state=True) diff --git a/gui/wxpython/vdigit/dialogs.py b/gui/wxpython/vdigit/dialogs.py index 8d617f62d8b..394bf19c598 100644 --- a/gui/wxpython/vdigit/dialogs.py +++ b/gui/wxpython/vdigit/dialogs.py @@ -401,7 +401,7 @@ def OnCancel(self, event): def OnApply(self, event): """Apply button pressed""" - for fid in self.cats.keys(): + for fid in list(self.cats.keys()): newfid = self.ApplyChanges(fid) if fid == self.fid and newfid > 0: self.fid = newfid diff --git a/gui/wxpython/vdigit/g.gui.vdigit.html b/gui/wxpython/vdigit/g.gui.vdigit.html index 766c0209270..c71182ec851 100644 --- a/gui/wxpython/vdigit/g.gui.vdigit.html +++ b/gui/wxpython/vdigit/g.gui.vdigit.html @@ -271,7 +271,7 @@

NOTES

v.build.

-GRASS GIS uses a topological vector format, meaning that a common boundary of +GRASS uses a topological vector format, meaning that a common boundary of two polygons is only stored once. When digitizing polygons it is thus important to be able to only draw each boundary once. When drawing a polygon adjacent to an existing polygon, one has to first split the existing boundary at the points where diff --git a/gui/wxpython/vdigit/g.gui.vdigit.md b/gui/wxpython/vdigit/g.gui.vdigit.md index f56df5b8bd1..7b165b19c56 100644 --- a/gui/wxpython/vdigit/g.gui.vdigit.md +++ b/gui/wxpython/vdigit/g.gui.vdigit.md @@ -220,7 +220,7 @@ If the digitizer crashes for some reason, the changes are automatically saved. Broken topology can be repaired by running *[v.build](v.build.md)*. -GRASS GIS uses a topological vector format, meaning that a common +GRASS uses a topological vector format, meaning that a common boundary of two polygons is only stored once. When digitizing polygons it is thus important to be able to only draw each boundary once. When drawing a polygon adjacent to an existing polygon, one has to first diff --git a/gui/wxpython/vdigit/g.gui.vdigit.py b/gui/wxpython/vdigit/g.gui.vdigit.py index 93f1278584c..2ccd99bcf33 100644 --- a/gui/wxpython/vdigit/g.gui.vdigit.py +++ b/gui/wxpython/vdigit/g.gui.vdigit.py @@ -146,7 +146,7 @@ def __init__(self, parent, vectorMap): id=wx.ID_ANY, size=(850, 600), style=wx.DEFAULT_FRAME_STYLE, - title=_("Vector Digitizer - GRASS GIS"), + title=_("Vector Digitizer - GRASS"), ) frame = VDigitMapDisplay(parent=frame, vectorMap=options["map"]) frame.Show() diff --git a/gui/wxpython/vnet/vnet_core.py b/gui/wxpython/vnet/vnet_core.py index 0fadf24b790..bc3c4afbda5 100644 --- a/gui/wxpython/vnet/vnet_core.py +++ b/gui/wxpython/vnet/vnet_core.py @@ -226,9 +226,8 @@ def _checkResultMapChanged(self, resultVectMap): parent=self, message=_( "Temporary map '%s' with result " - + "was changed outside vector network analysis tool.\n" - + "Showed result may not correspond " - + "original analysis result." + "was changed outside vector network analysis tool.\n" + "Showed result may not correspond to original analysis result." ) % resultVectMap.GetVectMapName(), caption=_("Result changed outside"), @@ -929,9 +928,8 @@ def _updateHistStepData(self, histStepData): parent=self.guiparent, message=_( "Input map '%s' for analysis was changed outside " - + "vector network analysis tool.\n" - + "Topology column may not " - + "correspond to changed situation." + "vector network analysis tool.\n" + "Topology column may not correspond to changed situation." ) % inpMap, caption=_("Input changed outside"), @@ -1003,8 +1001,8 @@ def NewTmpVectMapToHist(self, prefMapName): def AddTmpMapAnalysisMsg(mapName, tmp_maps): # TODO """Wraps AddTmpVectMap""" msg = _( - "Temporary map %s already exists.\n" - + "Do you want to continue in analysis and overwrite it?" + "Temporary map %s already exists.\n" + "Do you want to continue in analysis and overwrite it?" ) % (mapName + "@" + grass.gisenv()["MAPSET"]) return tmp_maps.AddTmpVectMap(mapName, msg) @@ -1071,9 +1069,8 @@ def ComputeNodes(self, activate): elif self.snapPts.VectMapState() == 0: dlg = wx.MessageDialog( message=_( - "Temporary map '%s' was changed outside " - + "vector analysis tool.\n" - "Do you really want to activate " + "snapping and overwrite it? " + "Temporary map '%s' was changed outside vector analysis tool.\n" + "Do you really want to activate snapping and overwrite it? " ) % self.snapPts.GetVectMapName(), caption=_("Overwrite map"), diff --git a/gui/wxpython/web_services/dialogs.py b/gui/wxpython/web_services/dialogs.py index 1240ffb88b6..34acb0f6754 100644 --- a/gui/wxpython/web_services/dialogs.py +++ b/gui/wxpython/web_services/dialogs.py @@ -24,6 +24,7 @@ import shutil from copy import deepcopy +from pathlib import Path import grass.script as gs from grass.script.task import cmdlist_to_tuple, cmdtuple_to_list @@ -676,7 +677,7 @@ def __del__(self): def _setRevertCapFiles(self, ws_cap_files): for ws, f in ws_cap_files.items(): - if os.path.isfile(f): + if Path(f).is_file(): shutil.copyfile(f, self.revert_ws_cap_files[ws]) else: # delete file content diff --git a/gui/wxpython/wxgui.py b/gui/wxpython/wxgui.py index 83fed965ecd..05212146fd2 100644 --- a/gui/wxpython/wxgui.py +++ b/gui/wxpython/wxgui.py @@ -36,7 +36,6 @@ # we get annoying "Debug: Adding duplicate image handler for 'Windows bitmap file'" # during start up, remove when not needed import wx.adv -import wx.html try: import wx.lib.agw.advancedsplash as SC @@ -63,7 +62,7 @@ def OnInit(self): :return: True """ # Internal and display name of the app (if supported by/on platform) - self.SetAppName("GRASS GIS") + self.SetAppName("GRASS") self.SetVendorName("The GRASS Development Team") # create splash screen diff --git a/gui/wxpython/wxplot/histogram.py b/gui/wxpython/wxplot/histogram.py index b65ebd03192..455f4fdef7c 100644 --- a/gui/wxpython/wxplot/histogram.py +++ b/gui/wxpython/wxplot/histogram.py @@ -151,7 +151,7 @@ def SetupHistogram(self): # set xlabel based on first raster map in list to be histogrammed # units = self.raster[self.rasterList[0]]["units"] - if units not in ("", "(none)") and units is not None: + if units not in {"", "(none)"} and units is not None: self.xlabel = _("Raster cell values %s") % units else: self.xlabel = _("Raster cell values") diff --git a/gui/wxpython/wxplot/profile.py b/gui/wxpython/wxplot/profile.py index 2402b8c33fe..537e59ff101 100644 --- a/gui/wxpython/wxplot/profile.py +++ b/gui/wxpython/wxplot/profile.py @@ -15,7 +15,6 @@ @author Michael Barton, Arizona State University """ -import os import sys import math import numpy as np @@ -316,9 +315,9 @@ def CreateDatalist(self, raster, coords): dist, elev = line.strip().split(" ") if ( dist is None - or dist in ("", "nan") + or dist in {"", "nan"} or elev is None - or elev in ("", "nan") + or elev in {"", "nan"} ): continue dist = float(dist) @@ -421,7 +420,7 @@ def SaveProfileToFile(self, event): path = dlg.GetPath() for r in self.rasterList: pfile.append(path + "_" + str(r.replace("@", "_")) + ".csv") - if os.path.exists(pfile[-1]): + if Path(pfile[-1]).exists(): dlgOv = wx.MessageDialog( self, message=_( diff --git a/gui/wxpython/xml/main_menu.xml b/gui/wxpython/xml/main_menu.xml index b6d21eea27a..8301ccbc420 100644 --- a/gui/wxpython/xml/main_menu.xml +++ b/gui/wxpython/xml/main_menu.xml @@ -1,7 +1,7 @@ - + diff --git a/gui/wxpython/xml/menudata_pyedit.xml b/gui/wxpython/xml/menudata_pyedit.xml index cb519d234e5..e793fd57ce9 100644 --- a/gui/wxpython/xml/menudata_pyedit.xml +++ b/gui/wxpython/xml/menudata_pyedit.xml @@ -79,7 +79,7 @@ - Display documentation for writing Python code in GRASS GIS + Display documentation for writing Python code in GRASS OnHelp @@ -94,17 +94,17 @@ - Display guidelines for submitting Python code to GRASS GIS + Display guidelines for submitting Python code to GRASS OnSubmittingHelp - Display instructions for submitting you code to GRASS GIS Addons repository + Display instructions for submitting you code to GRASS Addons repository OnAddonsHelp - Display information about getting support for writing Python code in GRASS GIS + Display information about getting support for writing Python code in GRASS OnSupport diff --git a/gui/wxpython/xml/module_tree.xml b/gui/wxpython/xml/module_tree.xml index c5d40ba08dc..edc2e2ae6c5 100644 --- a/gui/wxpython/xml/module_tree.xml +++ b/gui/wxpython/xml/module_tree.xml @@ -1,7 +1,7 @@ - + diff --git a/gui/wxpython/xml/wxgui_items.xml b/gui/wxpython/xml/wxgui_items.xml index ba4b30fa840..bd6f24c00e0 100644 --- a/gui/wxpython/xml/wxgui_items.xml +++ b/gui/wxpython/xml/wxgui_items.xml @@ -73,9 +73,9 @@ Close graphical user interface. - + OnCloseWindowOrExit - Close GUI and exit GRASS GIS shell. + Close GUI and exit GRASS shell. Ctrl+Q ID_EXIT @@ -100,14 +100,14 @@ g.manual -i - Display the HTML man pages of GRASS GIS + Display the HTML man pages of GRASS general,manual,help ID_HELP g.manual entry=wxGUI - Display the HTML man pages of GRASS GIS + Display the HTML man pages of GRASS general,manual,help @@ -116,9 +116,9 @@ Prints system information - + OnAboutGRASS - About GRASS GIS + About GRASS ID_ABOUT diff --git a/imagery/i.albedo/testsuite/test_i_albedo.py b/imagery/i.albedo/testsuite/test_i_albedo.py index 3806cd8bfc4..bc5a4a2dd8b 100644 --- a/imagery/i.albedo/testsuite/test_i_albedo.py +++ b/imagery/i.albedo/testsuite/test_i_albedo.py @@ -4,7 +4,7 @@ class TestIAlbedo(TestCase): - """Regression tests for the i.albedo GRASS GIS module.""" + """Regression tests for the i.albedo GRASS module.""" output_raster = "albedo_output" @@ -28,7 +28,7 @@ def _create_input_rasters(self, sensor, values=None): "aster": [f"test_b{i}" for i in [1, 3, 5, 6, 8, 9]], }.get(sensor) values = values or [0.1 * (i + 1) for i in range(len(bands))] - for band, value in zip(bands, values): + for band, value in zip(bands, values, strict=True): self.runModule("r.mapcalc", expression=f"{band} = {value}", overwrite=True) return bands diff --git a/imagery/i.biomass/testsuite/test_i_biomass.py b/imagery/i.biomass/testsuite/test_i_biomass.py index e4fd3732f39..9a204c58aed 100644 --- a/imagery/i.biomass/testsuite/test_i_biomass.py +++ b/imagery/i.biomass/testsuite/test_i_biomass.py @@ -5,7 +5,7 @@ class TestIBiomass(TestCase): - """Regression tests for the i.biomass GRASS GIS module.""" + """Regression tests for the i.biomass GRASS module.""" input_rasters = { "fpar": "test_fpar", diff --git a/imagery/i.cca/testsuite/test_i_cca.py b/imagery/i.cca/testsuite/test_i_cca.py index 5a4f0f98439..0c53409e5e5 100644 --- a/imagery/i.cca/testsuite/test_i_cca.py +++ b/imagery/i.cca/testsuite/test_i_cca.py @@ -4,7 +4,7 @@ class TestICCA(TestCase): - """Regression and invariance tests for the i.cca GRASS GIS module.""" + """Regression and invariance tests for the i.cca GRASS module.""" group_name = "cca_group" subgroup_name = "cca_subgroup" diff --git a/imagery/i.cluster/testsuite/test_i_cluster.py b/imagery/i.cluster/testsuite/test_i_cluster.py index e91ed1134ad..aeab5241afe 100644 --- a/imagery/i.cluster/testsuite/test_i_cluster.py +++ b/imagery/i.cluster/testsuite/test_i_cluster.py @@ -63,7 +63,7 @@ def _signature_exists(self): "sig", "sig", ) - return os.path.isfile(sig_file_path) + return Path(sig_file_path).is_file() def test_cluster_creation(self): """Test the creation of a signature file and validate the number of classes.""" @@ -97,7 +97,7 @@ def test_sample_interval(self): """Test that sample interval affects the number of cells sampled""" sample_interval = (2, 2) report_file = "sample_report.txt" - self.addCleanup(os.remove, report_file) if os.path.exists(report_file) else None + self.addCleanup(os.remove, report_file) if Path(report_file).exists() else None self.assertModule( "i.cluster", @@ -109,7 +109,7 @@ def test_sample_interval(self): sample=sample_interval, overwrite=True, ) - self.assertTrue(os.path.isfile(report_file)) + self.assertTrue(Path(report_file).is_file()) content = Path(report_file).read_text() expected_cells = (100 // sample_interval[0]) * (100 // sample_interval[1]) match = re.search(r"Sample size:\s+(\d+)", content) @@ -123,8 +123,8 @@ def test_convergence_parameters(self): high_percent = 99.5 low_report = "low_percent_report.txt" high_report = "high_percent_report.txt" - self.addCleanup(os.remove, low_report) if os.path.exists(low_report) else None - self.addCleanup(os.remove, high_report) if os.path.exists(high_report) else None + self.addCleanup(os.remove, low_report) if Path(low_report).exists() else None + self.addCleanup(os.remove, high_report) if Path(high_report).exists() else None self.assertModule( "i.cluster", group=self.group_name, @@ -198,14 +198,14 @@ def minimum_pairwise_distance(self, means): """Calculate the minimum pairwise Euclidean distance between cluster means.""" min_dist = float("inf") for a, b in combinations(means, 2): - dist = math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b))) + dist = math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b, strict=False))) min_dist = min(min_dist, dist) return min_dist def test_separation_parameter1(self): """Test the effect of the separation parameter on cluster separation quality.""" report_file = "separation_report.txt" - self.addCleanup(os.remove, report_file) if os.path.exists(report_file) else None + self.addCleanup(os.remove, report_file) if Path(report_file).exists() else None sig_file_default = self.signature_file + "_sep0" self.assertModule( "i.cluster", diff --git a/imagery/i.fft/testsuite/test_i_fft.py b/imagery/i.fft/testsuite/test_i_fft.py index b5c86a3c621..557c1218810 100644 --- a/imagery/i.fft/testsuite/test_i_fft.py +++ b/imagery/i.fft/testsuite/test_i_fft.py @@ -5,7 +5,7 @@ class TestIFFT(TestCase): - """Regression tests for the i.fft GRASS GIS module.""" + """Regression tests for the i.fft GRASS module.""" input_raster = "test_input" real_output = "fft_real" diff --git a/imagery/i.group/main.c b/imagery/i.group/main.c index c7d79851c49..568c8a69319 100644 --- a/imagery/i.group/main.c +++ b/imagery/i.group/main.c @@ -37,8 +37,8 @@ static int remove_subgroup_files(char group[INAME_LEN], char subgroup[INAME_LEN], char **rasters, int k); static void print_subgroups(const char *group, const char *mapset, - enum OutputFormat format, JSON_Array *root_array); -static void list_files_json(const struct Ref *ref, JSON_Array *root_array); + enum OutputFormat format, G_JSON_Array *root_array); +static void list_files_json(const struct Ref *ref, G_JSON_Array *root_array); int main(int argc, char *argv[]) { @@ -54,8 +54,8 @@ int main(int argc, char *argv[]) enum OutputFormat format; - JSON_Value *root_value = NULL; - JSON_Array *root_array = NULL; + G_JSON_Value *root_value = NULL; + G_JSON_Array *root_array = NULL; G_gisinit(argv[0]); @@ -506,7 +506,7 @@ static int remove_subgroup_files(char group[INAME_LEN], } static void print_subgroups(const char *group, const char *mapset, - enum OutputFormat format, JSON_Array *root_array) + enum OutputFormat format, G_JSON_Array *root_array) { int subgs_num, i; int len, tot_len; @@ -565,7 +565,7 @@ static void print_subgroups(const char *group, const char *mapset, * \param ref group reference (set with I_get_group_ref()) * \param root_array JSON array to which data will be appended. */ -static void list_files_json(const struct Ref *ref, JSON_Array *root_array) +static void list_files_json(const struct Ref *ref, G_JSON_Array *root_array) { int i; char map_str[1024]; diff --git a/imagery/i.ifft/testsuite/test_i_ifft.py b/imagery/i.ifft/testsuite/test_i_ifft.py index 8ec4f692072..aa68ad82ca5 100644 --- a/imagery/i.ifft/testsuite/test_i_ifft.py +++ b/imagery/i.ifft/testsuite/test_i_ifft.py @@ -5,7 +5,7 @@ class TestIIFFT(TestCase): - """Regression tests for the i.ifft GRASS GIS module.""" + """Regression tests for the i.ifft GRASS module.""" real_input = "real_input_raster" imag_input = "imag_input_raster" diff --git a/imagery/i.landsat.toar/testsuite/test_i_landsat_toar.py b/imagery/i.landsat.toar/testsuite/test_i_landsat_toar.py index cffa36709be..2786feed1af 100644 --- a/imagery/i.landsat.toar/testsuite/test_i_landsat_toar.py +++ b/imagery/i.landsat.toar/testsuite/test_i_landsat_toar.py @@ -84,7 +84,7 @@ def tearDownClass(cls): test_rasters += [f"{cls.output_prefix}{suffix}{b}" for b in bands] all_rasters = input_rasters + output_rasters + test_rasters cls.runModule("g.remove", flags="f", type="raster", name=all_rasters) - if os.path.exists(cls.metfile): + if Path(cls.metfile).exists(): os.remove(cls.metfile) def test_reflectance_and_temperature_conversion_with_scale(self): diff --git a/imagery/i.modis.qc/i.modis.qc.html b/imagery/i.modis.qc/i.modis.qc.html index 8f96364a937..d4b93576e93 100644 --- a/imagery/i.modis.qc/i.modis.qc.html +++ b/imagery/i.modis.qc/i.modis.qc.html @@ -504,7 +504,7 @@

NOTES

will not give the expected range of values in the different QC bits. Therefore, before using i.modis.qc, the user needs to set the NULL value in QC bands back to zero (i.e.: r.null map=QC_band null=0) or just edit the metadata with GDAL -utilities before importing into GRASS GIS. This is a known issue for MOD11A2 +utilities before importing into GRASS. This is a known issue for MOD11A2 (8-day LST product), but other MODIS products might be affected as well.

TODO

diff --git a/imagery/i.modis.qc/i.modis.qc.md b/imagery/i.modis.qc/i.modis.qc.md index 5ba3e6bab8c..3ef14843e35 100644 --- a/imagery/i.modis.qc/i.modis.qc.md +++ b/imagery/i.modis.qc/i.modis.qc.md @@ -476,7 +476,7 @@ those QC bands will not give the expected range of values in the different QC bits. Therefore, before using *i.modis.qc*, the user needs to set the NULL value in QC bands back to zero (i.e.: `r.null map=QC_band null=0`) or just edit the metadata with GDAL -utilities before importing into GRASS GIS. This is a known issue for +utilities before importing into GRASS. This is a known issue for MOD11A2 (8-day LST product), but other MODIS products might be affected as well. diff --git a/imagery/i.ortho.photo/i.ortho.photo/i.ortho.photo.html b/imagery/i.ortho.photo/i.ortho.photo/i.ortho.photo.html index 842bf6ee906..dbe94ec1f56 100644 --- a/imagery/i.ortho.photo/i.ortho.photo/i.ortho.photo.html +++ b/imagery/i.ortho.photo/i.ortho.photo/i.ortho.photo.html @@ -41,7 +41,7 @@

DESCRIPTION

-The ortho-rectification procedure in GRASS GIS places the image pixels on +The ortho-rectification procedure in GRASS places the image pixels on the surface of the earth by matching the coordinate system of the aerial image in pixels (image coordinate system) and the coordinate system of the camera sensor in millimetres (photo coordinate system) diff --git a/imagery/i.ortho.photo/i.ortho.photo/i.ortho.photo.md b/imagery/i.ortho.photo/i.ortho.photo/i.ortho.photo.md index ffa4a402584..51f4cb662b9 100644 --- a/imagery/i.ortho.photo/i.ortho.photo/i.ortho.photo.md +++ b/imagery/i.ortho.photo/i.ortho.photo/i.ortho.photo.md @@ -26,7 +26,7 @@ performed separately by running the appropriate modules. - *Ortho-rectification* 1. Ortho-rectify imagery group: [i.ortho.rectify](i.ortho.rectify.md) -The ortho-rectification procedure in GRASS GIS places the image pixels +The ortho-rectification procedure in GRASS places the image pixels on the surface of the earth by matching the coordinate system of the aerial image in pixels (*image coordinate system*) and the coordinate system of the camera sensor in millimetres (*photo coordinate system*) diff --git a/imagery/i.ortho.photo/i.ortho.rectify/main.c b/imagery/i.ortho.photo/i.ortho.rectify/main.c index 81412e816e2..7f0e19bd291 100644 --- a/imagery/i.ortho.photo/i.ortho.rectify/main.c +++ b/imagery/i.ortho.photo/i.ortho.rectify/main.c @@ -151,8 +151,12 @@ int main(int argc, char *argv[]) interpolate = menu[method].method; G_strip(grp->answer); - strcpy(group.name, grp->answer); - strcpy(extension, ext->answer); + if (G_strlcpy(group.name, grp->answer, sizeof(group.name)) >= + sizeof(group.name)) + G_fatal_error(_("Group name <%s> is too long"), grp->answer); + if (G_strlcpy(extension, ext->answer, sizeof(extension)) >= + sizeof(extension)) + G_fatal_error(_("Extension <%s> is too long"), ext->answer); seg_mb = NULL; if (mem->answer) { @@ -281,7 +285,10 @@ int main(int argc, char *argv[]) if (!ref_list[i]) continue; - strcpy(result, group.group_ref.file[i].name); + if (G_strlcpy(result, group.group_ref.file[i].name, + sizeof(result)) >= sizeof(result)) + G_fatal_error(_("Map name <%s> is too long"), + group.group_ref.file[i].name); strcat(result, extension); if (G_legal_filename(result) < 0) diff --git a/imagery/i.ortho.photo/i.ortho.rectify/readcell.c b/imagery/i.ortho.photo/i.ortho.rectify/readcell.c index ebe937b6b4b..f9ccc597d04 100644 --- a/imagery/i.ortho.photo/i.ortho.rectify/readcell.c +++ b/imagery/i.ortho.photo/i.ortho.rectify/readcell.c @@ -135,7 +135,7 @@ block *get_block(struct cache *c, int idx) c->grid[idx] = p; c->refs[replace] = idx; - if (lseek(c->fd, offset, SEEK_SET) < 0) + if (lseek(c->fd, offset, SEEK_SET) == -1) G_fatal_error(_("Error seeking on segment file")); if (read(c->fd, p, sizeof(block)) < 0) diff --git a/imagery/i.ortho.photo/i.ortho.rectify/target.c b/imagery/i.ortho.photo/i.ortho.rectify/target.c index 5c74e8529c4..4fc08f58b1b 100644 --- a/imagery/i.ortho.photo/i.ortho.rectify/target.c +++ b/imagery/i.ortho.photo/i.ortho.rectify/target.c @@ -33,11 +33,13 @@ int get_target(char *group) snprintf(buf, sizeof(buf), _("Mapset <%s> in target project (location) <%s> - "), mapset, location); - strcat(buf, stat == 0 ? _("permission denied") : _("not found")); + (void)G_strlcat(buf, stat == 0 ? _("permission denied") : _("not found"), + sizeof(buf)); error: - strcat(buf, "\n"); - strcat(buf, _("Please run i.target for group ")); - strcat(buf, group); + (void)G_strlcat(buf, "\n", sizeof(buf)); + (void)G_strlcat(buf, _("Please run i.target for group "), sizeof(buf)); + if (G_strlcat(buf, group, sizeof(buf)) >= sizeof(buf)) + G_fatal_error(_("Internal error: error message too long")); G_fatal_error("%s", buf); return 1; /* never reached */ } diff --git a/imagery/i.rectify/readcell.c b/imagery/i.rectify/readcell.c index 7cff35df684..9b36f340b60 100644 --- a/imagery/i.rectify/readcell.c +++ b/imagery/i.rectify/readcell.c @@ -119,7 +119,7 @@ block *get_block(struct cache *c, int idx) c->grid[idx] = p; c->refs[replace] = idx; - if (lseek(c->fd, offset, SEEK_SET) < 0) + if (lseek(c->fd, offset, SEEK_SET) == -1) G_fatal_error(_("Error seeking on segment file")); if (read(c->fd, p, sizeof(block)) < 0) diff --git a/imagery/i.rectify/testsuite/test_i_rectify.py b/imagery/i.rectify/testsuite/test_i_rectify.py index 4de3283a4b7..873f23dc30a 100644 --- a/imagery/i.rectify/testsuite/test_i_rectify.py +++ b/imagery/i.rectify/testsuite/test_i_rectify.py @@ -66,7 +66,7 @@ def setUpClass(cls): cls.group_path = os.path.join( env["GISDBASE"], env["LOCATION_NAME"], env["MAPSET"], "group", cls.group ) - os.makedirs(cls.group_path, exist_ok=True) + Path(cls.group_path).mkdir(exist_ok=True, parents=True) cls.points_path = os.path.join(cls.group_path, "POINTS") cls.runModule( "i.target", @@ -90,10 +90,10 @@ def tearDownClass(cls): """Remove input rasters, group, and temporary files.""" cls.runModule("g.remove", type="raster", name=cls.tmp_rasters, flags="f") cls.runModule("g.remove", type="group", name=cls.group, flags="f") - if os.path.exists(cls.points_path): + if Path(cls.points_path).exists(): os.remove(cls.points_path) for f in cls.tmp_files: - if os.path.exists(f): + if Path(f).exists(): os.remove(f) def _run_and_check( diff --git a/imagery/i.segment/cluster.c b/imagery/i.segment/cluster.c index 9f88ce8d32e..e88bd1d35bf 100644 --- a/imagery/i.segment/cluster.c +++ b/imagery/i.segment/cluster.c @@ -22,6 +22,9 @@ #include #include #include +#include +#include + #include #include #include @@ -421,7 +424,11 @@ CELL cluster_bands(struct globals *globals) G_fatal_error(_("Too many objects: integer overflow")); /* rewind temp file */ - lseek(cfd, 0, SEEK_SET); + if (lseek(cfd, 0, SEEK_SET) == -1) { + int err = errno; + G_fatal_error(_("File read/write operation failed: %s (%d)"), + strerror(err), err); + } /**************************************************** * PASS 2 * diff --git a/imagery/i.signatures/testsuite/test_i_signatures.py b/imagery/i.signatures/testsuite/test_i_signatures.py index 651d510ed03..17a83238a17 100644 --- a/imagery/i.signatures/testsuite/test_i_signatures.py +++ b/imagery/i.signatures/testsuite/test_i_signatures.py @@ -9,9 +9,9 @@ for details """ -import os import shutil import json +from pathlib import Path from grass.gunittest.case import TestCase from grass.gunittest.main import test @@ -34,27 +34,27 @@ def setUpClass(cls): cls.sigdirs = [] # As signatures are created directly not via signature creation # tools, we must ensure signature directories exist - os.makedirs(f"{cls.mpath}/signatures/sig/", exist_ok=True) - os.makedirs(f"{cls.mpath}/signatures/sigset/", exist_ok=True) - os.makedirs(f"{cls.mpath}/signatures/libsvm/", exist_ok=True) + Path(f"{cls.mpath}/signatures/sig/").mkdir(exist_ok=True, parents=True) + Path(f"{cls.mpath}/signatures/sigset/").mkdir(exist_ok=True, parents=True) + Path(f"{cls.mpath}/signatures/libsvm/").mkdir(exist_ok=True, parents=True) # Fake signature of sig type cls.sig_name1 = tempname(10) sig_dir1 = f"{cls.mpath}/signatures/sig/{cls.sig_name1}" - os.makedirs(sig_dir1) + Path(sig_dir1).mkdir(parents=True) cls.sigdirs.append(sig_dir1) sigfile_name1 = f"{sig_dir1}/sig" open(sigfile_name1, "a").close() # Fake signature of sigset type cls.sig_name2 = tempname(10) sig_dir2 = f"{cls.mpath}/signatures/sigset/{cls.sig_name2}" - os.makedirs(sig_dir2) + Path(sig_dir2).mkdir(parents=True) cls.sigdirs.append(sig_dir2) sigfile_name2 = f"{sig_dir2}/sig" open(sigfile_name2, "a").close() # Fake signature of libsvm type cls.sig_name3 = tempname(10) sig_dir3 = f"{cls.mpath}/signatures/libsvm/{cls.sig_name3}" - os.makedirs(sig_dir3) + Path(sig_dir3).mkdir(parents=True) cls.sigdirs.append(sig_dir3) sigfile_name3 = f"{sig_dir3}/sig" open(sigfile_name3, "a").close() @@ -151,48 +151,48 @@ def setUpClass(cls): cls.sigdirs = [] # As signatures are created directly not via signature creation # tools, we must ensure signature directories exist - os.makedirs(f"{cls.mpath}/signatures/sig/", exist_ok=True) - os.makedirs(f"{cls.mpath}/signatures/sigset/", exist_ok=True) - os.makedirs(f"{cls.mpath}/signatures/libsvm/", exist_ok=True) + Path(f"{cls.mpath}/signatures/sig/").mkdir(exist_ok=True, parents=True) + Path(f"{cls.mpath}/signatures/sigset/").mkdir(exist_ok=True, parents=True) + Path(f"{cls.mpath}/signatures/libsvm/").mkdir(exist_ok=True, parents=True) # sig cls.sig_name1 = tempname(10) sig_dir1 = f"{cls.mpath}/signatures/sig/{cls.sig_name1}" - os.makedirs(sig_dir1) + Path(sig_dir1).mkdir(parents=True) cls.sigdirs.append(sig_dir1) sigfile_name1 = f"{sig_dir1}/sig" open(sigfile_name1, "a").close() # sig cls.sig_name2 = tempname(10) sig_dir2 = f"{cls.mpath}/signatures/sig/{cls.sig_name2}" - os.makedirs(sig_dir2) + Path(sig_dir2).mkdir(parents=True) cls.sigdirs.append(sig_dir2) sigfile_name2 = f"{sig_dir2}/sig" open(sigfile_name2, "a").close() # sigset cls.sig_name3 = tempname(10) sig_dir3 = f"{cls.mpath}/signatures/sigset/{cls.sig_name3}" - os.makedirs(sig_dir3) + Path(sig_dir3).mkdir(parents=True) cls.sigdirs.append(sig_dir3) sigfile_name3 = f"{sig_dir3}/sig" open(sigfile_name3, "a").close() # sigset cls.sig_name4 = tempname(10) sig_dir4 = f"{cls.mpath}/signatures/sigset/{cls.sig_name4}" - os.makedirs(sig_dir4) + Path(sig_dir4).mkdir(parents=True) cls.sigdirs.append(sig_dir4) sigfile_name4 = f"{sig_dir4}/sig" open(sigfile_name4, "a").close() # libsvm cls.sig_name5 = tempname(10) sig_dir5 = f"{cls.mpath}/signatures/libsvm/{cls.sig_name5}" - os.makedirs(sig_dir5) + Path(sig_dir5).mkdir(parents=True) cls.sigdirs.append(sig_dir5) sigfile_name5 = f"{sig_dir5}/sig" open(sigfile_name5, "a").close() # libsvm cls.sig_name6 = tempname(10) sig_dir6 = f"{cls.mpath}/signatures/libsvm/{cls.sig_name6}" - os.makedirs(sig_dir6) + Path(sig_dir6).mkdir(parents=True) cls.sigdirs.append(sig_dir6) sigfile_name6 = f"{sig_dir6}/sig" open(sigfile_name6, "a").close() diff --git a/imagery/i.smap/model.c b/imagery/i.smap/model.c index dd567cfe5bb..4dd3ab5f761 100644 --- a/imagery/i.smap/model.c +++ b/imagery/i.smap/model.c @@ -141,7 +141,7 @@ void extract(DCELL ***img, /* multispectral image, img[band][i][j] */ ll[i][j][m] = subll[0]; } /* compute mixture likelihood */ - else { + else if (C->nsubclasses > 1) { /* find the most likely subclass */ for (k = 0; k < C->nsubclasses; k++) { if (k == 0) @@ -157,6 +157,9 @@ void extract(DCELL ***img, /* multispectral image, img[band][i][j] */ ll[i][j][m] = log(subsum) + maxlike; } + else { + ll[i][j][m] = 0.0; + } } } } diff --git a/imagery/i.smap/testsuite/test_i_smap.py b/imagery/i.smap/testsuite/test_i_smap.py index 3342a4f72a6..0af340025e6 100644 --- a/imagery/i.smap/testsuite/test_i_smap.py +++ b/imagery/i.smap/testsuite/test_i_smap.py @@ -4,7 +4,7 @@ class TestISmap(TestCase): - """Regression tests for i.smap GRASS GIS module.""" + """Regression tests for i.smap GRASS module.""" group_name = "test_smap_group" subgroup_name = "test_smap_subgroup" diff --git a/imagery/i.svm.train/testsuite/test_i_svm_train.py b/imagery/i.svm.train/testsuite/test_i_svm_train.py index 2aa5f4eaba3..af2ab843172 100644 --- a/imagery/i.svm.train/testsuite/test_i_svm_train.py +++ b/imagery/i.svm.train/testsuite/test_i_svm_train.py @@ -9,10 +9,10 @@ for details. """ -import os import unittest import ctypes import shutil +from pathlib import Path from grass.script import core as grass from grass.gunittest.case import TestCase @@ -210,19 +210,19 @@ def test_creation_of_misc_files(self): cpath = ctypes.create_string_buffer(GPATH_MAX) G_file_name_misc(cpath, sigdir, "version", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) + self.assertTrue(Path(misc_file).is_file()) G_file_name_misc(cpath, sigdir, "sig", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) + self.assertTrue(Path(misc_file).is_file()) G_file_name_misc(cpath, sigdir, "cats", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) + self.assertTrue(Path(misc_file).is_file()) G_file_name_misc(cpath, sigdir, "colr", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) + self.assertTrue(Path(misc_file).is_file()) G_file_name_misc(cpath, sigdir, "history", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) + self.assertTrue(Path(misc_file).is_file()) @unittest.skipIf(shutil.which("i.svm.train") is None, "i.svm.train not found.") def test_dont_fail_if_misc_files_missing(self): @@ -243,19 +243,19 @@ def test_dont_fail_if_misc_files_missing(self): cpath = ctypes.create_string_buffer(GPATH_MAX) G_file_name_misc(cpath, sigdir, "version", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) + self.assertTrue(Path(misc_file).is_file()) G_file_name_misc(cpath, sigdir, "sig", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) + self.assertTrue(Path(misc_file).is_file()) G_file_name_misc(cpath, sigdir, "cats", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) + self.assertTrue(Path(misc_file).is_file()) G_file_name_misc(cpath, sigdir, "colr", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertFalse(os.path.isfile(misc_file)) + self.assertFalse(Path(misc_file).is_file()) G_file_name_misc(cpath, sigdir, "history", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) + self.assertTrue(Path(misc_file).is_file()) @unittest.skipIf(shutil.which("i.svm.train") is None, "i.svm.train not found.") def test_rescaling(self): @@ -276,23 +276,23 @@ def test_rescaling(self): cpath = ctypes.create_string_buffer(GPATH_MAX) G_file_name_misc(cpath, sigdir, "version", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) + self.assertTrue(Path(misc_file).is_file()) G_file_name_misc(cpath, sigdir, "sig", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) + self.assertTrue(Path(misc_file).is_file()) G_file_name_misc(cpath, sigdir, "cats", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) + self.assertTrue(Path(misc_file).is_file()) G_file_name_misc(cpath, sigdir, "colr", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertFalse(os.path.isfile(misc_file)) + self.assertFalse(Path(misc_file).is_file()) G_file_name_misc(cpath, sigdir, "history", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) + self.assertTrue(Path(misc_file).is_file()) G_file_name_misc(cpath, sigdir, "scale", sigfile, self.mapset_name) misc_file = utils.decode(cpath.value) - self.assertTrue(os.path.isfile(misc_file)) - with open(misc_file) as rf: + self.assertTrue(Path(misc_file).is_file()) + with Path(misc_file).open() as rf: lines = rf.readlines() M, R = lines[0].strip().split(" ") self.assertTrue(float(M) > -1 and float(M) < 1) diff --git a/imagery/i.target/main.c b/imagery/i.target/main.c index 6765ad8517a..c92ab759891 100644 --- a/imagery/i.target/main.c +++ b/imagery/i.target/main.c @@ -78,8 +78,10 @@ int main(int argc, char *argv[]) G_fatal_error(_("Group must exist in the current mapset")); } else { - strcpy(group_name, group->answer); /* FIXME for buffer overflow (have - the parser check that?) */ + if (G_strlcpy(group_name, group->answer, sizeof(group_name)) >= + sizeof(group_name)) { + G_fatal_error(_("Group name <%s> is too long"), group->answer); + } } /* if no setting options are given, print the current target info */ diff --git a/imagery/i.target/testsuite/test_i_target.py b/imagery/i.target/testsuite/test_i_target.py index 043b78173e8..17916e129f0 100644 --- a/imagery/i.target/testsuite/test_i_target.py +++ b/imagery/i.target/testsuite/test_i_target.py @@ -1,11 +1,12 @@ import os +from pathlib import Path import grass.script as gs from grass.gunittest.case import TestCase from grass.gunittest.main import test class TestITarget(TestCase): - """Regression tests for the i.target GRASS GIS module.""" + """Regression tests for the i.target GRASS module.""" @classmethod def setUpClass(cls): @@ -45,11 +46,11 @@ def test_current_location_flag(self): group = self.groups[0] target_file = self._target_path(group) - if os.path.exists(target_file): + if Path(target_file).exists(): os.remove(target_file) self.assertModule("i.target", flags="c", group=group) - self.assertTrue(os.path.exists(target_file)) + self.assertTrue(Path(target_file).exists()) current_env = gs.gisenv() loc, mset = self._get_target_values(group) diff --git a/imagery/i.topo.corr/test_i.topo.corr_synthetic_DEM_NC.sh b/imagery/i.topo.corr/test_i.topo.corr_synthetic_DEM_NC.sh index c645d910dad..e2da7978752 100755 --- a/imagery/i.topo.corr/test_i.topo.corr_synthetic_DEM_NC.sh +++ b/imagery/i.topo.corr/test_i.topo.corr_synthetic_DEM_NC.sh @@ -6,7 +6,7 @@ # grass ~/grassdata/nc_spm_08_grass7/user1 if test "$GISBASE" = ""; then - echo "You must be in GRASS GIS to run this program." + echo "You must be in GRASS to run this program." exit fi diff --git a/imagery/i.zc/testsuite/test_i_zc.py b/imagery/i.zc/testsuite/test_i_zc.py index 0ee27d35933..b836f106a05 100644 --- a/imagery/i.zc/testsuite/test_i_zc.py +++ b/imagery/i.zc/testsuite/test_i_zc.py @@ -5,7 +5,7 @@ class TestIZC(TestCase): - """Regression tests for the i.zc GRASS GIS module.""" + """Regression tests for the i.zc GRASS module.""" input_raster = "input_raster" output_raster = "output_raster" diff --git a/imagery/imageryintro.html b/imagery/imageryintro.html index 21b2279becc..aabc3529be0 100644 --- a/imagery/imageryintro.html +++ b/imagery/imageryintro.html @@ -1,14 +1,14 @@ - +

Image processing in general

-GRASS GIS provides a powerful suite of tools for the processing and +GRASS provides a powerful suite of tools for the processing and analysis of geospatial raster data, including satellite imagery and aerial photography. Its image processing capabilities encompass a broad range of preprocessing operations, such as data import, georeferencing, radiometric calibration, and atmospheric correction. It is particularly suited for handling Earth observation data, enabling the analysis of -multispectral and temporal datasets. GRASS GIS supports advanced +multispectral and temporal datasets. GRASS supports advanced functionalities such as image classification, sensor fusion, and point cloud statistics. The calculation of vegetation indices further enables analyses of environmental, agricultural, and land cover dynamics. An @@ -34,7 +34,7 @@

Image processing in general

in 8 to 16 bits. DNs can be turned back into physical values by applying the reverse formula (x = (y - b) / a).

-The GRASS GIS module i.landsat.toar +The GRASS module i.landsat.toar easily transforms Landsat DN to radiance-at-sensor (top of atmosphere, TOA). The equivalent module for ASTER data is i.aster.toar. @@ -53,7 +53,7 @@

Image processing in general

times, atmospheric need to be removed converting at-sensor values which are top of atmosphere to surface reflectance values.

-In GRASS GIS, there are two ways to apply atmospheric correction for +In GRASS, there are two ways to apply atmospheric correction for satellite imagery. A simple, less accurate way for Landsat is with i.landsat.toar, using the DOS correction method. The more accurate way is using @@ -64,7 +64,7 @@

Image processing in general

data correction is the proper level of correction to calculate vegetation indices.

-In GRASS GIS, image data are identical to raster data. +In GRASS, image data are identical to raster data. However, a couple of commands are explicitly dedicated to image processing. The geographic boundaries of the raster/imagery file are described by the north, south, east, and west fields. These values @@ -114,7 +114,7 @@

Semantic label information

a different group with identical semantic labels.
-GRASS GIS band references scheme
+GRASS band references scheme
New enhanced classification workflow involving semantic labels. @@ -286,7 +286,7 @@

Energy balance

See also