diff --git a/gui/wxpython/xml/toolboxes.xml b/gui/wxpython/xml/toolboxes.xml index 6070a7bb49b..7859ea56724 100644 --- a/gui/wxpython/xml/toolboxes.xml +++ b/gui/wxpython/xml/toolboxes.xml @@ -1214,6 +1214,9 @@ + + + diff --git a/raster/r.fill.stats/r.fill.stats.html b/raster/r.fill.stats/r.fill.stats.html index 5b534c9a3cd..6cffdc04182 100644 --- a/raster/r.fill.stats/r.fill.stats.html +++ b/raster/r.fill.stats/r.fill.stats.html @@ -504,7 +504,8 @@

SEE ALSO

r.surf.idw, v.surf.bspline, v.surf.idw, -v.surf.rst +v.surf.rst, +v.fill.holes

diff --git a/scripts/r.fillnulls/r.fillnulls.html b/scripts/r.fillnulls/r.fillnulls.html index 61f16930807..240d3259394 100644 --- a/scripts/r.fillnulls/r.fillnulls.html +++ b/scripts/r.fillnulls/r.fillnulls.html @@ -128,7 +128,8 @@

SEE ALSO

r.mapcalc, r.resamp.bspline, v.surf.bspline, -v.surf.rst +v.surf.rst, +v.fill.holes

AUTHORS

diff --git a/vector/Makefile b/vector/Makefile index 1a8f551a438..f195ce3aafb 100644 --- a/vector/Makefile +++ b/vector/Makefile @@ -22,6 +22,7 @@ SUBDIRS = \ v.edit \ v.extract \ v.extrude \ + v.fill.holes \ v.generalize \ v.hull \ v.info \ diff --git a/vector/v.clean/v.clean.html b/vector/v.clean/v.clean.html index 8688700e3dc..35d500a7bf8 100644 --- a/vector/v.clean/v.clean.html +++ b/vector/v.clean/v.clean.html @@ -375,6 +375,7 @@

SEE ALSO

v.build, g.gui.vdigit, v.edit, +v.fill.holes, v.generalize diff --git a/vector/v.fill.holes/Makefile b/vector/v.fill.holes/Makefile new file mode 100644 index 00000000000..f9f82be44b4 --- /dev/null +++ b/vector/v.fill.holes/Makefile @@ -0,0 +1,12 @@ +MODULE_TOPDIR = ../.. + +PGM = v.fill.holes + +LIBES = $(VECTORLIB) $(DBMILIB) $(GISLIB) +DEPENDENCIES = $(VECTORDEP) $(DBMIDEP) $(GISDEP) +EXTRA_INC = $(VECT_INC) +EXTRA_CFLAGS = $(VECT_CFLAGS) + +include $(MODULE_TOPDIR)/include/Make/Module.make + +default: cmd diff --git a/vector/v.fill.holes/examples.ipynb b/vector/v.fill.holes/examples.ipynb new file mode 100644 index 00000000000..28d86332b0a --- /dev/null +++ b/vector/v.fill.holes/examples.ipynb @@ -0,0 +1,301 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Graphics for Documentation of _v.fill.holes_\n", + "\n", + "Requires _pngquant_, _optipng_ and _ImageMagic_ (_mogrify_, _montage_)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "\n", + "from IPython.display import Image\n", + "\n", + "import grass.script as gs\n", + "import grass.jupyter as gj" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gj.init(\"~/grassdata/nc_spm_08_grass7/user1\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Explanation Plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Use data and import code from tests.\n", + "sys.path.append(\"./tests\")\n", + "import conftest\n", + "from pathlib import Path" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "work_dir = Path(\".\")\n", + "conftest.import_data(\n", + " path=work_dir,\n", + " areas_name=\"data\",\n", + " areas_with_space_in_between=\"dissolve_data\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!v.fill.holes input=data output=data_filled" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!g.region vector=data grow=3 res=1\n", + "text_position = (75,5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot = gj.Map(use_region=True, width=700)\n", + "plot.d_background(color=\"white\")\n", + "plot.d_vect(map=\"data\")\n", + "plot.d_text(at=text_position, color=\"black\", text=\"(a) Original\")\n", + "plot.save(\"original.png\")\n", + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot = gj.Map(use_region=True, width=700)\n", + "plot.d_background(color=\"white\")\n", + "plot.d_vect(map=\"data_filled\")\n", + "plot.d_text(at=text_position, color=\"black\", text=\"(b) Filled\")\n", + "plot.save(\"new.png\")\n", + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "example_1 = \"v_fill_holes_filled.png\"\n", + "!montage original.png new.png -tile 2x1 -geometry +0+0 {example_1}\n", + "Image(example_1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!v.fill.holes input=dissolve_data output=dissolve_data_filled\n", + "!v.db.update map=dissolve_data column=name value=\"area\"\n", + "!v.dissolve input=dissolve_data output=dissolved_data column=name\n", + "!v.fill.holes input=dissolved_data output=dissolved_data_filled" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!g.region vector=dissolve_data grow=2 res=1\n", + "\n", + "text_position = (75,5)\n", + "\n", + "plot = gj.Map(use_region=True, width=700)\n", + "plot.d_background(color=\"white\")\n", + "plot.d_vect(map=\"dissolve_data\")\n", + "plot.d_text(at=text_position, color=\"black\", text=\"(a) Original\")\n", + "plot.save(\"original.png\")\n", + "\n", + "plot = gj.Map(use_region=True, width=700)\n", + "plot.d_background(color=\"white\")\n", + "plot.d_vect(map=\"dissolve_data_filled\")\n", + "plot.d_text(at=text_position, color=\"black\", text=\"() Filled without dissolve\")\n", + "plot.save(\"not_working.png\")\n", + "\n", + "plot = gj.Map(use_region=True, width=700)\n", + "plot.d_background(color=\"white\")\n", + "plot.d_vect(map=\"dissolved_data\")\n", + "plot.d_text(at=text_position, color=\"black\", text=\"(b) Dissloved\")\n", + "plot.save(\"dissolved.png\")\n", + "\n", + "plot = gj.Map(use_region=True, width=700)\n", + "plot.d_background(color=\"white\")\n", + "plot.d_vect(map=\"dissolved_data_filled\")\n", + "plot.d_text(at=text_position, color=\"black\", text=\"(c) Filled\")\n", + "plot.save(\"filled.png\")\n", + "\n", + "example_2 = \"v_fill_holes_filled_with_dissolve.png\"\n", + "!montage original.png dissolved.png filled.png -tile 3x1 -geometry +0+0 {example_2}\n", + "Image(example_2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!v.extract --overwrite input=lakes where=\"FTYPE != 'ROCK/ISLAND'\" output=lakes_only --qq" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!g.region n=243300 s=242950 w=647200 e=648000" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot = gj.Map(use_region=True, width=700)\n", + "plot.d_background(color=\"white\")\n", + "plot.d_vect(map=\"lakes_only\", legend_label=\"Original\")\n", + "plot.d_legend_vect(flags=\"b\", at=(60,10))\n", + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!v.fill.holes input=lakes_only output=lakes_filled" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot = gj.Map(use_region=True, width=700)\n", + "plot.d_background(color=\"white\")\n", + "plot.d_vect(map=\"lakes_filled\", legend_label=\"Filled\")\n", + "plot.d_legend_vect(flags=\"b\", at=(60,10))\n", + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!v.dissolve input=lakes_filled column=NAME output=lakes_dissolved --qq" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot = gj.Map(use_region=True, width=700)\n", + "plot.d_background(color=\"white\")\n", + "plot.d_vect(map=\"lakes_dissolved\", legend_label=\"Dissolved\")\n", + "plot.d_legend_vect(flags=\"b\", at=(60,10))\n", + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot = gj.Map(use_region=True, width=1024)\n", + "plot.d_background(color=\"#E28A2B\")\n", + "plot.d_vect(map=\"lakes_filled\", color=\"none\", fill_color=\"#384C6B\", legend_label=\"Filled\")\n", + "plot.d_vect(map=\"lakes_only\", color=\"#859BBA\", fill_color=\"none\", width=2, legend_label=\"Original\")\n", + "plot.d_legend_vect(flags=\"b\", at=(80,85), fontsize=22, symbol_size=35)\n", + "filename = \"v_fill_holes.png\"\n", + "plot.save(filename)\n", + "!mogrify -trim {filename}\n", + "!pngquant --ext \".png\" -f {filename}\n", + "!optipng -o7 {filename}\n", + "Image(filename)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/vector/v.fill.holes/main.c b/vector/v.fill.holes/main.c new file mode 100644 index 00000000000..8f8bf20a0bf --- /dev/null +++ b/vector/v.fill.holes/main.c @@ -0,0 +1,212 @@ + +/**************************************************************** + * + * MODULE: v.fill.holes + * + * AUTHOR: Vaclav Petras + * + * PURPOSE: Fill holes in an area, i.e., preserve only its outer boundary + * + * COPYRIGHT: (C) 2023 by Vaclav Petras and the GRASS Development Team + * + * This program is free software under the GNU General + * Public License (>=v2). Read the file COPYING that + * comes with GRASS for details. + * + ****************************************************************/ + +#include +#include +#include +#include +#include +#include + +struct VFillHolesParameters { + struct GModule *module; + struct Option *input; + struct Option *output; + struct Option *field; + struct Option *cats; + struct Option *where; +}; + +int main(int argc, char *argv[]) +{ + struct Map_info input; + struct Map_info output; + int open3d; + + G_gisinit(argv[0]); + + struct VFillHolesParameters options; + + options.module = G_define_module(); + G_add_keyword(_("vector")); + G_add_keyword(_("geometry")); + G_add_keyword(_("fill")); + G_add_keyword(_("exterior")); + G_add_keyword(_("ring")); + G_add_keyword(_("perimeter")); + options.module->description = + _("Fill holes in areas by keeping only outer boundaries"); + + options.input = G_define_standard_option(G_OPT_V_INPUT); + + options.field = G_define_standard_option(G_OPT_V_FIELD); + + options.cats = G_define_standard_option(G_OPT_V_CATS); + + options.where = G_define_standard_option(G_OPT_DB_WHERE); + + options.output = G_define_standard_option(G_OPT_V_OUTPUT); + + if (G_parser(argc, argv)) + exit(EXIT_FAILURE); + + Vect_check_input_output_name(options.input->answer, options.output->answer, + G_FATAL_EXIT); + Vect_set_open_level(2); + + if (1 > Vect_open_old2(&input, options.input->answer, "", + options.field->answer)) + G_fatal_error(_("Unable to open vector map <%s>"), + options.input->answer); + + /* Check if old vector is 3D. We should preserve 3D data. */ + if (Vect_is_3d(&input)) + open3d = WITH_Z; + else + open3d = WITHOUT_Z; + + /* Set error handler for input vector map */ + Vect_set_error_handler_io(&input, NULL); + + /* Open new vector for reading/writing */ + if (0 > Vect_open_new(&output, options.output->answer, open3d)) { + G_fatal_error(_("Unable to create vector map <%s>"), + options.output->answer); + } + + /* Set error handler for output vector map */ + Vect_set_error_handler_io(NULL, &output); + + int field = Vect_get_field_number(&input, options.field->answer); + + if (field <= 0 && options.cats->answer) + G_fatal_error(_("Option %s cannot be combined with %s=%s"), + options.cats->key, options.field->key, + options.field->answer); + if (field <= 0 && options.where->answer) + G_fatal_error(_("Option %s cannot be combined with %s=%s"), + options.where->key, options.field->key, + options.field->answer); + + /* Copy header and history data from old to new map */ + Vect_copy_head_data(&input, &output); + Vect_hist_copy(&input, &output); + Vect_hist_command(&output); + + // Set category constraint. + struct cat_list *constraint_cat_list = NULL; + + if (field > 0) + constraint_cat_list = Vect_cats_set_constraint( + &input, field, options.where->answer, options.cats->answer); + + /* Create and initialize struct's where to store points/lines and categories + */ + struct line_pnts *points = Vect_new_line_struct(); + struct line_cats *area_cats = Vect_new_cats_struct(); + struct line_cats *boundary_cats = Vect_new_cats_struct(); + + struct ilist *all_cats = Vect_new_list(); + struct ilist *field_cats = Vect_new_list(); + struct ilist *area_boundaries = Vect_new_list(); + + plus_t num_areas = Vect_get_num_areas(&input); + plus_t num_lines = Vect_get_num_lines(&input); + // Used as index. 0th element is unused. + bool *line_written_out = G_calloc(num_lines + 1, sizeof(bool)); + + G_percent(0, num_areas, 1); + for (plus_t area = 1; area <= num_areas; area++) { + G_percent(area, num_areas, 1); + + int centroid = Vect_get_area_centroid(&input, area); + + if (!centroid) + continue; + + Vect_read_line(&input, points, area_cats, centroid); + + if (constraint_cat_list && + !Vect_cats_in_constraint(area_cats, field, constraint_cat_list)) { + continue; + } + Vect_write_line(&output, GV_CENTROID, points, area_cats); + + Vect_get_area_boundaries(&input, area, area_boundaries); + for (int i = 0; i < area_boundaries->n_values; i++) { + int boundary_id = abs(area_boundaries->value[i]); + if (line_written_out[boundary_id]) + continue; + Vect_read_line(&input, points, boundary_cats, boundary_id); + Vect_write_line(&output, GV_BOUNDARY, points, boundary_cats); + line_written_out[boundary_id] = true; + } + + if (field > 0) { + Vect_field_cat_get(area_cats, field, field_cats); + Vect_list_append_list(all_cats, field_cats); + } + } + + Vect_destroy_cats_struct(boundary_cats); + Vect_destroy_cats_struct(area_cats); + Vect_destroy_line_struct(points); + + /* Let's get vector layers db connections information */ + struct field_info *input_info = NULL; + + if (field > 0 && all_cats->n_values) + input_info = Vect_get_field2(&input, options.field->answer); + + if (input_info) { + G_verbose_message(_("Copying attributes for layer <%s>"), + options.field->answer); + + struct field_info *output_info = + Vect_default_field_info(&output, field, NULL, GV_1TABLE); + /* Create database for new vector map */ + dbDriver *driver = db_start_driver_open_database(output_info->driver, + output_info->database); + Vect_map_add_dblink(&output, output_info->number, output_info->name, + output_info->table, input_info->key, + output_info->database, output_info->driver); + + /* Copy attribute table data */ + if (db_copy_table_by_ints( + input_info->driver, input_info->database, input_info->table, + output_info->driver, + Vect_subst_var(output_info->database, &output), + output_info->table, input_info->key, all_cats->value, + all_cats->n_values) == DB_FAILED) + G_fatal_error( + _("Unable to copy attribute table to vector map <%s>"), + options.output->answer); + db_close_database_shutdown_driver(driver); + } + + Vect_destroy_list(field_cats); + Vect_destroy_list(all_cats); + + Vect_build(&output); + Vect_close(&input); + + /* Build topology for vector map and close them */ + + Vect_close(&output); + + exit(EXIT_SUCCESS); +} diff --git a/vector/v.fill.holes/tests/conftest.py b/vector/v.fill.holes/tests/conftest.py new file mode 100644 index 00000000000..a86a07cc043 --- /dev/null +++ b/vector/v.fill.holes/tests/conftest.py @@ -0,0 +1,227 @@ +"""Fixture for v.fill.holes test""" + +from types import SimpleNamespace + +import pytest + +import grass.script as gs + +DATA = """\ +VERTI: +C 1 1 + 26.4740753 19.30631132 + 1 1 +B 3 + 35.7009422 5.30814651 + 39.02877384 19.25061356 + 35.75831861 33.47996264 +B 3 + 35.75831861 33.47996264 + 26.6354698 35.71764254 + 20.03718292 32.73406934 +C 1 1 + 42.19882036 26.59479373 + 1 3 +B 8 1 + 31.98581981 29.69311974 + 27.51046001 31.70129401 + 26.07604982 27.45543984 + 29.23175224 23.78334976 + 33.82186485 23.20958568 + 35.02676941 26.76692295 + 34.51038174 29.52099052 + 31.98581981 29.69311974 + 1 4 +B 7 1 + 42.02669114 33.25045701 + 39.55950561 28.602968 + 43.00209007 28.37346236 + 45.64140482 31.6439176 + 45.18239356 35.48813691 + 41.96931473 33.99635031 + 42.02669114 33.25045701 + 1 5 +B 7 1 + 23.83836993 17.2998157 + 21.83019566 21.60304627 + 18.27285839 20.45551812 + 17.46958868 17.18506288 + 21.14167877 12.65232668 + 24.3547576 14.48837172 + 23.83836993 17.2998157 + 1 6 +B 7 1 + 31.52680855 17.64407414 + 28.60061176 12.8244559 + 33.36285359 9.66875349 + 36.63330882 13.5129728 + 36.69068523 17.24243929 + 32.33007825 18.21783822 + 31.52680855 17.64407414 + 1 7 +B 7 + 46.35860991 38.87334496 + 33.25872825 43.10254145 + 27.62998922 41.83894698 + 17.32020703 35.66456488 + 11.83505828 29.34659251 + 8.81966238 23.22964653 + 11.43072178 20.57027093 +B 2 + 20.03718292 32.73406934 + 11.43072178 20.57027093 +B 2 + 35.75831861 33.47996264 + 46.35860991 38.87334496 +C 1 1 + 30.9900018 38.79483301 + 1 9 +B 12 1 + 20.38144137 3.7589835 + 2.42754667 6.02567852 + 0.12531832 16.15548326 + 2.65776951 31.69552462 + 12.90268566 49.07734866 + 30.86006679 52.18535694 + 51.58012194 49.53779433 + 57.91124991 36.76042699 + 58.71702983 19.14838012 + 50.88945344 6.02567852 + 41.91076287 0.38521906 + 38.64148309 4.21799476 + 1 10 +B 2 + 11.43072178 20.57027093 + 20.38144137 3.7589835 +B 2 + 20.38144137 3.7589835 + 35.7009422 5.30814651 +B 5 + 46.35860991 38.87334496 + 46.81762117 30.95540071 + 46.2725453 14.68918915 + 41.45292706 12.10725081 + 38.64148309 4.21799476 +B 2 + 38.64148309 4.21799476 + 35.7009422 5.30814651 +C 1 1 + 21.00365167 45.50889472 + 1 11 +""" + +AREAS_WITH_SPACE_GEOMETRY = """\ +VERTI: +B 5 1 + 8.45818088 33.40191653 + 0.60764185 25.08168713 + 0.33924735 9.78320082 + 10.26984373 1.06037967 + 26.37351354 0.12099893 + 1 1 +B 7 1 + 26.37351354 0.12099893 + 42.61138059 4.28111363 + 43.81915583 22.26354492 + 35.16343331 33.26771928 + 23.01858233 34.07290277 + 13.62477494 34.1400014 + 8.45818088 33.40191653 + 1 1 +C 1 1 + 10.40404098 15.75497837 + 1 3 +C 1 1 + 28.85616264 22.12934767 + 1 4 +B 6 1 + 13.62477494 25.282983 + 22.95148371 26.42365962 + 28.05097915 18.90861371 + 31.8085021 10.38708843 + 30.26523374 2.93914115 + 25.03154106 3.74432464 + 1 5 +B 2 1 + 8.45818088 33.40191653 + 13.62477494 25.282983 + 1 2 +B 3 1 + 13.62477494 25.282983 + 22.68308921 15.0168935 + 25.03154106 3.74432464 + 1 2 +B 2 1 + 25.03154106 3.74432464 + 26.37351354 0.12099893 + 1 2 +""" + +AREAS_WITH_SPACE_ATTRIBUTES = """\ +cat,name +3,"Left plot" +4,"Right plot" +""" + +AREAS_WITH_SPACE_ATTRIBUTE_TYPES = """\ +"Integer","String" +""" + + +def import_data(path, areas_name, areas_with_space_in_between): + gs.write_command( + "v.in.ascii", input="-", output=areas_name, stdin=DATA, format="standard" + ) + attributes = path / "test.csv" + attributes.write_text(AREAS_WITH_SPACE_ATTRIBUTES) + attribute_types = path / "test.csvt" + attribute_types.write_text(AREAS_WITH_SPACE_ATTRIBUTE_TYPES) + # Attributes need to be created first because no vector map of the same name + # can exist when table is imported (interally using v.in.ogr and vector part + # is deleted). + gs.run_command("db.in.ogr", input=attributes, output=areas_with_space_in_between) + gs.write_command( + "v.in.ascii", + input="-", + output=areas_with_space_in_between, + stdin=AREAS_WITH_SPACE_GEOMETRY, + format="standard", + ) + # Our old cat column is now called cat_, so we need to rename it to cat, + # but that's possible only on vector map level, so connect, rename, and + # reconnect to create indices. + gs.run_command( + "v.db.connect", + map=areas_with_space_in_between, + table=areas_with_space_in_between, + ) + gs.run_command( + "v.db.renamecolumn", map=areas_with_space_in_between, column=("cat_", "cat") + ) + gs.run_command( + "v.db.connect", + map=areas_with_space_in_between, + table=areas_with_space_in_between, + flags="o", + ) + + +@pytest.fixture(scope="module") +def area_dataset(tmp_path_factory): + """Create a session and fill mapset with data""" + tmp_path = tmp_path_factory.mktemp("area_dataset") + location = "test" + + areas_name = "test_areas" + areas_with_space_in_between = "areas_with_space_in_between" + + gs.core._create_location_xy(tmp_path, location) # pylint: disable=protected-access + with gs.setup.init(tmp_path / location): + import_data( + path=tmp_path, + areas_name=areas_name, + areas_with_space_in_between=areas_with_space_in_between, + ) + yield SimpleNamespace( + name=areas_name, areas_with_space_in_between=areas_with_space_in_between + ) diff --git a/vector/v.fill.holes/tests/v_fill_holes_test.py b/vector/v.fill.holes/tests/v_fill_holes_test.py new file mode 100644 index 00000000000..5bac9c6519b --- /dev/null +++ b/vector/v.fill.holes/tests/v_fill_holes_test.py @@ -0,0 +1,52 @@ +"""Test v.fill.holes outputs""" + +import json + +import grass.script as gs + + +def test_removal(area_dataset): + """Check that holes are removed""" + output = "test" + gs.run_command("v.fill.holes", input=area_dataset.name, output=output) + original_info = gs.vector_info(area_dataset.name) + info = gs.vector_info(output) + removed = 4 + assert info["nodes"] == original_info["nodes"] - removed + assert info["points"] == original_info["points"] + assert info["lines"] == original_info["lines"] + assert info["boundaries"] == original_info["boundaries"] - removed + assert info["centroids"] == original_info["centroids"] + assert info["areas"] == original_info["areas"] - removed + assert info["islands"] == original_info["islands"] - removed + assert info["primitives"] == original_info["primitives"] - removed + + +def test_no_change(area_dataset): + """Check that space in between is not changed including attributes""" + output = "no_change" + gs.run_command( + "v.fill.holes", input=area_dataset.areas_with_space_in_between, output=output + ) + original_info = gs.vector_info(area_dataset.areas_with_space_in_between) + info = gs.vector_info(output) + for item in [ + "nodes", + "points", + "lines", + "boundaries", + "centroids", + "areas", + "islands", + "primitives", + ]: + assert info[item] == original_info[item], item + + records = json.loads(gs.read_command("v.db.select", map=output, format="json"))[ + "records" + ] + assert len(records) == 2 + assert records[0]["cat"] == 3 + assert records[0]["name"] == "Left plot" + assert records[1]["cat"] == 4 + assert records[1]["name"] == "Right plot" diff --git a/vector/v.fill.holes/v.fill.holes.html b/vector/v.fill.holes/v.fill.holes.html new file mode 100644 index 00000000000..7bb7ef1cbad --- /dev/null +++ b/vector/v.fill.holes/v.fill.holes.html @@ -0,0 +1,119 @@ +

DESCRIPTION

+ +v.fill.holes fills empty spaces inside areas, specifically +it preserves areas with centroids while areas without centroids, +which typically represent holes, are removed. + +v.fill.holes goes over all areas in a vector map +and it preserves only outer boundaries of each area +while removing inner boundaries which are considered holes. +The holes become part of the area which contained them. +No boundaries of these holes are preserved. + +
+ + Several areas, some with holes (left) and the same areas but with holes filled (right) +
+ + Figure: Holes inside areas are removed. (a) Original areas with holes and (b) the same areas but with holes filled. + +
+ +In case areas have empty space in between them, +i.e., there are holes in the overall coverage, but not in the areas themselves, +v.fill.holes can't assign this empty space to either of these areas +because it does not know which area this empty space should belong to. +If the space needs to be filled, this can be resolved by merging the areas +around the empty space into one by dissolving their common boundaries. +This turns the empty space into a hole inside one single area +which turns the situation into a case of one area with a hole. + +
+ + Two areas with empty space in between (left), both areas merged (middle), and the empty space filled (right) +
+ + Figure: Empty space in between two areas does not belong to either area, + so it is filled only after the boundaries between areas are dissolved, + i.e., areas merged into one. + (a) Original areas with space in between, + (b) one area with a hole after dissolving the common boundary, and + (c) hole filled. + +
+ +

Topology

+ +Strictly speaking, in the GRASS topological model, an area is a closed boundary +(or a series of connected closed boundaries) which may have a centroid. +If it has a centroid, it is rendered as a filled area in displays and +this is what is usually considered an area from the user perspective. +These are the areas where v.fill.holes preserves the associated outer boundary (or boundaries). +Other closed boundaries, i.e., those without a centroid, are not carried over to the output. +All other features are removed including points and lines. + +

Attributes

+ +If a specific layer is selected, attributes for that layer are preserved +for the areas based on the category or categories associated with each area. +By default, layer number 1 is selected. +In case there are attribute tables associated with other layers or attributes +associated with categories of other features than areas with centroids, +this attribute data is not carried over to the output just like the +corresponding geometries. + +

EXAMPLE

+ +The lakes vector map in the North Carolina sample dataset +represents islands inside lakes as areas distinguished by attributes. +To demonstrate v.fill.holes, we will first extract only the +lakes which will create holes where the islands were located. +Then, we will fill the holes created in the lakes to get +the whole perimeter of the lakes including islands. + +Remove the islands by extracting everything else (results in holes): + +
+v.extract input=lakes where="FTYPE != 'ROCK/ISLAND'" output=lakes_only
+
+ +Remove the holes: + +
+v.fill.holes input=lakes_only output=lakes_filled
+
+ +
+ + Lake without holes overlapping with the lines marking the original +
+ + Figure: The filled lake (blue) and borders of the original lakes with islands removed (light blue). + Figure shows a smaller area in the north of the data extent. + +
+ +

SEE ALSO

+ +
    +
  • + v.dissolve + for removing common boundaries based on attributes, +
  • +
  • + v.clean + for removing topological issues, +
  • +
  • + r.fillnulls + for filling empty spaces in raster maps using interpolation, +
  • +
  • + r.fill.stats + for filling empty spaces in raster maps using statistics. +
  • +
+ +

AUTHOR

+ +Vaclav Petras, NCSU Center for Geospatial Analytics, GeoForAll Lab diff --git a/vector/v.fill.holes/v_fill_holes.png b/vector/v.fill.holes/v_fill_holes.png new file mode 100644 index 00000000000..03ecdddcb87 Binary files /dev/null and b/vector/v.fill.holes/v_fill_holes.png differ diff --git a/vector/v.fill.holes/v_fill_holes_filled.png b/vector/v.fill.holes/v_fill_holes_filled.png new file mode 100644 index 00000000000..1826d392826 Binary files /dev/null and b/vector/v.fill.holes/v_fill_holes_filled.png differ diff --git a/vector/v.fill.holes/v_fill_holes_filled_with_dissolve.png b/vector/v.fill.holes/v_fill_holes_filled_with_dissolve.png new file mode 100644 index 00000000000..75e9688930d Binary files /dev/null and b/vector/v.fill.holes/v_fill_holes_filled_with_dissolve.png differ