main.c 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. /****************************************************************
  2. *
  3. * MODULE: v.generalize
  4. *
  5. * AUTHOR(S): Daniel Bundala
  6. * OGR support by Martin Landa <landa.martin gmail.com> (2009)
  7. * Markus Metz: preserve areas and area attributes
  8. *
  9. * PURPOSE: Module for line simplification and smoothing
  10. *
  11. * COPYRIGHT: (C) 2007-2010, 2012 by the GRASS Development Team
  12. *
  13. * This program is free software under the GNU General
  14. * Public License (>=v2). Read the file COPYING that
  15. * comes with GRASS for details.
  16. *
  17. ****************************************************************/
  18. #include <stdio.h>
  19. #include <stdlib.h>
  20. #include <string.h>
  21. #include <grass/gis.h>
  22. #include <grass/vector.h>
  23. #include <grass/glocale.h>
  24. #include "misc.h"
  25. #include "operators.h"
  26. #define DOUGLAS 0
  27. #define LANG 1
  28. #define VERTEX_REDUCTION 2
  29. #define REUMANN 3
  30. #define BOYLE 4
  31. #define DISTANCE_WEIGHTING 5
  32. #define CHAIKEN 6
  33. #define HERMITE 7
  34. #define SNAKES 8
  35. #define DOUGLAS_REDUCTION 9
  36. #define SLIDING_AVERAGING 10
  37. #define NETWORK 100
  38. #define DISPLACEMENT 101
  39. int main(int argc, char *argv[])
  40. {
  41. struct Map_info In, Out, Error;
  42. struct line_pnts *Points;
  43. struct line_cats *Cats;
  44. int i, type, iter;
  45. struct GModule *module; /* GRASS module for parsing arguments */
  46. struct Option *map_in, *map_out, *error_out, *thresh_opt, *method_opt,
  47. *look_ahead_opt;
  48. struct Option *iterations_opt, *cat_opt, *alpha_opt, *beta_opt, *type_opt;
  49. struct Option *field_opt, *where_opt, *reduction_opt, *slide_opt;
  50. struct Option *angle_thresh_opt, *degree_thresh_opt,
  51. *closeness_thresh_opt;
  52. struct Option *betweeness_thresh_opt;
  53. struct Flag *notab_flag, *loop_support_flag;
  54. int with_z;
  55. int total_input, total_output; /* Number of points in the input/output map respectively */
  56. double thresh, alpha, beta, reduction, slide, angle_thresh;
  57. double degree_thresh, closeness_thresh, betweeness_thresh;
  58. int method;
  59. int look_ahead, iterations;
  60. int loop_support;
  61. int layer;
  62. int n_lines;
  63. int simplification, mask_type;
  64. struct cat_list *cat_list = NULL;
  65. char *s, *descriptions;
  66. /* initialize GIS environment */
  67. G_gisinit(argv[0]); /* reads grass env, stores program name to G_program_name() */
  68. /* initialize module */
  69. module = G_define_module();
  70. G_add_keyword(_("vector"));
  71. G_add_keyword(_("generalization"));
  72. G_add_keyword(_("simplification"));
  73. G_add_keyword(_("smoothing"));
  74. G_add_keyword(_("displacement"));
  75. G_add_keyword(_("network generalization"));
  76. module->description = _("Performs vector based generalization.");
  77. /* Define the different options as defined in gis.h */
  78. map_in = G_define_standard_option(G_OPT_V_INPUT);
  79. field_opt = G_define_standard_option(G_OPT_V_FIELD_ALL);
  80. type_opt = G_define_standard_option(G_OPT_V_TYPE);
  81. type_opt->options = "line,boundary,area";
  82. type_opt->answer = "line,boundary,area";
  83. type_opt->guisection = _("Selection");
  84. map_out = G_define_standard_option(G_OPT_V_OUTPUT);
  85. error_out = G_define_standard_option(G_OPT_V_OUTPUT);
  86. error_out->key = "error";
  87. error_out->required = NO;
  88. error_out->description =
  89. _("Error map of all lines and boundaries not being generalized due to topology issues or over-simplification");
  90. method_opt = G_define_option();
  91. method_opt->key = "method";
  92. method_opt->type = TYPE_STRING;
  93. method_opt->required = YES;
  94. method_opt->multiple = NO;
  95. method_opt->options =
  96. "douglas,douglas_reduction,lang,reduction,reumann,boyle,sliding_averaging,distance_weighting,chaiken,hermite,snakes,network,displacement";
  97. descriptions = NULL;
  98. G_asprintf(&descriptions,
  99. "douglas;%s;"
  100. "douglas_reduction;%s;"
  101. "lang;%s;"
  102. "reduction;%s;"
  103. "reumann;%s;"
  104. "boyle;%s;"
  105. "sliding_averaging;%s;"
  106. "distance_weighting;%s;"
  107. "chaiken;%s;"
  108. "hermite;%s;"
  109. "snakes;%s;"
  110. "network;%s;"
  111. "displacement;%s;",
  112. _("Douglas-Peucker Algorithm"),
  113. _("Douglas-Peucker Algorithm with reduction parameter"),
  114. _("Lang Simplification Algorithm"),
  115. _("Vertex Reduction Algorithm eliminates points close to each other"),
  116. _("Reumann-Witkam Algorithm"),
  117. _("Boyle's Forward-Looking Algorithm"),
  118. _("McMaster's Sliding Averaging Algorithm"),
  119. _("McMaster's Distance-Weighting Algorithm"),
  120. _("Chaiken's Algorithm"),
  121. _("Interpolation by Cubic Hermite Splines"),
  122. _("Snakes method for line smoothing"),
  123. _("Network generalization"),
  124. _("Displacement of lines close to each other"));
  125. method_opt->descriptions = G_store(descriptions);
  126. method_opt->description = _("Generalization algorithm");
  127. thresh_opt = G_define_option();
  128. thresh_opt->key = "threshold";
  129. thresh_opt->type = TYPE_DOUBLE;
  130. thresh_opt->required = YES;
  131. thresh_opt->options = "0-1000000000";
  132. thresh_opt->description = _("Maximal tolerance value");
  133. look_ahead_opt = G_define_option();
  134. look_ahead_opt->key = "look_ahead";
  135. look_ahead_opt->type = TYPE_INTEGER;
  136. look_ahead_opt->required = NO;
  137. look_ahead_opt->answer = "7";
  138. look_ahead_opt->description = _("Look-ahead parameter");
  139. reduction_opt = G_define_option();
  140. reduction_opt->key = "reduction";
  141. reduction_opt->type = TYPE_DOUBLE;
  142. reduction_opt->required = NO;
  143. reduction_opt->answer = "50";
  144. reduction_opt->options = "0-100";
  145. reduction_opt->description =
  146. _("Percentage of the points in the output of 'douglas_reduction' algorithm");
  147. slide_opt = G_define_option();
  148. slide_opt->key = "slide";
  149. slide_opt->type = TYPE_DOUBLE;
  150. slide_opt->required = NO;
  151. slide_opt->answer = "0.5";
  152. slide_opt->options = "0-1";
  153. slide_opt->description =
  154. _("Slide of computed point toward the original point");
  155. angle_thresh_opt = G_define_option();
  156. angle_thresh_opt->key = "angle_thresh";
  157. angle_thresh_opt->type = TYPE_DOUBLE;
  158. angle_thresh_opt->required = NO;
  159. angle_thresh_opt->answer = "3";
  160. angle_thresh_opt->options = "0-180";
  161. angle_thresh_opt->description =
  162. _("Minimum angle between two consecutive segments in Hermite method");
  163. degree_thresh_opt = G_define_option();
  164. degree_thresh_opt->key = "degree_thresh";
  165. degree_thresh_opt->type = TYPE_INTEGER;
  166. degree_thresh_opt->required = NO;
  167. degree_thresh_opt->answer = "0";
  168. degree_thresh_opt->description =
  169. _("Degree threshold in network generalization");
  170. closeness_thresh_opt = G_define_option();
  171. closeness_thresh_opt->key = "closeness_thresh";
  172. closeness_thresh_opt->type = TYPE_DOUBLE;
  173. closeness_thresh_opt->required = NO;
  174. closeness_thresh_opt->answer = "0";
  175. closeness_thresh_opt->options = "0-1";
  176. closeness_thresh_opt->description =
  177. _("Closeness threshold in network generalization");
  178. betweeness_thresh_opt = G_define_option();
  179. betweeness_thresh_opt->key = "betweeness_thresh";
  180. betweeness_thresh_opt->type = TYPE_DOUBLE;
  181. betweeness_thresh_opt->required = NO;
  182. betweeness_thresh_opt->answer = "0";
  183. betweeness_thresh_opt->description =
  184. _("Betweeness threshold in network generalization");
  185. alpha_opt = G_define_option();
  186. alpha_opt->key = "alpha";
  187. alpha_opt->type = TYPE_DOUBLE;
  188. alpha_opt->required = NO;
  189. alpha_opt->answer = "1.0";
  190. alpha_opt->description = _("Snakes alpha parameter");
  191. beta_opt = G_define_option();
  192. beta_opt->key = "beta";
  193. beta_opt->type = TYPE_DOUBLE;
  194. beta_opt->required = NO;
  195. beta_opt->answer = "1.0";
  196. beta_opt->description = _("Snakes beta parameter");
  197. iterations_opt = G_define_option();
  198. iterations_opt->key = "iterations";
  199. iterations_opt->type = TYPE_INTEGER;
  200. iterations_opt->required = NO;
  201. iterations_opt->answer = "1";
  202. iterations_opt->description = _("Number of iterations");
  203. cat_opt = G_define_standard_option(G_OPT_V_CATS);
  204. cat_opt->guisection = _("Selection");
  205. where_opt = G_define_standard_option(G_OPT_DB_WHERE);
  206. where_opt->guisection = _("Selection");
  207. loop_support_flag = G_define_flag();
  208. loop_support_flag->key = 'l';
  209. loop_support_flag->label = _("Disable loop support");
  210. loop_support_flag->description = _("Do not modify end points of lines forming a closed loop");
  211. notab_flag = G_define_standard_flag(G_FLG_V_TABLE);
  212. notab_flag->description = _("Do not copy attributes");
  213. notab_flag->guisection = _("Attributes");
  214. /* options and flags parser */
  215. if (G_parser(argc, argv))
  216. exit(EXIT_FAILURE);
  217. thresh = atof(thresh_opt->answer);
  218. look_ahead = atoi(look_ahead_opt->answer);
  219. alpha = atof(alpha_opt->answer);
  220. beta = atof(beta_opt->answer);
  221. reduction = atof(reduction_opt->answer);
  222. iterations = atoi(iterations_opt->answer);
  223. slide = atof(slide_opt->answer);
  224. angle_thresh = atof(angle_thresh_opt->answer);
  225. degree_thresh = atof(degree_thresh_opt->answer);
  226. closeness_thresh = atof(closeness_thresh_opt->answer);
  227. betweeness_thresh = atof(betweeness_thresh_opt->answer);
  228. mask_type = type_mask(type_opt);
  229. G_debug(3, "Method: %s", method_opt->answer);
  230. s = method_opt->answer;
  231. if (strcmp(s, "douglas") == 0)
  232. method = DOUGLAS;
  233. else if (strcmp(s, "lang") == 0)
  234. method = LANG;
  235. else if (strcmp(s, "reduction") == 0)
  236. method = VERTEX_REDUCTION;
  237. else if (strcmp(s, "reumann") == 0)
  238. method = REUMANN;
  239. else if (strcmp(s, "boyle") == 0)
  240. method = BOYLE;
  241. else if (strcmp(s, "distance_weighting") == 0)
  242. method = DISTANCE_WEIGHTING;
  243. else if (strcmp(s, "chaiken") == 0)
  244. method = CHAIKEN;
  245. else if (strcmp(s, "hermite") == 0)
  246. method = HERMITE;
  247. else if (strcmp(s, "snakes") == 0)
  248. method = SNAKES;
  249. else if (strcmp(s, "douglas_reduction") == 0)
  250. method = DOUGLAS_REDUCTION;
  251. else if (strcmp(s, "sliding_averaging") == 0)
  252. method = SLIDING_AVERAGING;
  253. else if (strcmp(s, "network") == 0)
  254. method = NETWORK;
  255. else if (strcmp(s, "displacement") == 0) {
  256. method = DISPLACEMENT;
  257. /* we can displace only the lines */
  258. mask_type = GV_LINE;
  259. }
  260. else {
  261. G_fatal_error(_("Unknown method"));
  262. exit(EXIT_FAILURE);
  263. }
  264. /* simplification or smoothing? */
  265. switch (method) {
  266. case DOUGLAS:
  267. case DOUGLAS_REDUCTION:
  268. case LANG:
  269. case VERTEX_REDUCTION:
  270. case REUMANN:
  271. simplification = 1;
  272. break;
  273. default:
  274. simplification = 0;
  275. break;
  276. }
  277. Points = Vect_new_line_struct();
  278. Cats = Vect_new_cats_struct();
  279. Vect_check_input_output_name(map_in->answer, map_out->answer,
  280. G_FATAL_EXIT);
  281. Vect_set_open_level(2);
  282. if (Vect_open_old2(&In, map_in->answer, "", field_opt->answer) < 1)
  283. G_fatal_error(_("Unable to open vector map <%s>"), map_in->answer);
  284. if (Vect_get_num_primitives(&In, mask_type) == 0) {
  285. G_warning(_("No lines found in input map <%s>"), map_in->answer);
  286. Vect_close(&In);
  287. exit(EXIT_SUCCESS);
  288. }
  289. with_z = Vect_is_3d(&In);
  290. if (0 > Vect_open_new(&Out, map_out->answer, with_z)) {
  291. Vect_close(&In);
  292. G_fatal_error(_("Unable to create vector map <%s>"), map_out->answer);
  293. }
  294. if(error_out->answer)
  295. if (0 > Vect_open_new(&Error, error_out->answer, with_z)) {
  296. Vect_close(&In);
  297. G_fatal_error(_("Unable to create error vector map <%s>"), error_out->answer);
  298. }
  299. Vect_copy_head_data(&In, &Out);
  300. Vect_hist_copy(&In, &Out);
  301. Vect_hist_command(&Out);
  302. total_input = total_output = 0;
  303. layer = Vect_get_field_number(&In, field_opt->answer);
  304. /* parse filter options */
  305. if (layer > 0)
  306. cat_list = Vect_cats_set_constraint(&In, layer,
  307. where_opt->answer, cat_opt->answer);
  308. if (method == DISPLACEMENT) {
  309. /* modifies only lines, all other features including boundaries are preserved */
  310. /* options where, cats, and layer are respected */
  311. G_message(_("Displacement..."));
  312. snakes_displacement(&In, &Out, thresh, alpha, beta, 1.0, 10.0,
  313. iterations, cat_list, layer);
  314. }
  315. /* TODO: rearrange code below. It's really messy */
  316. if (method == NETWORK) {
  317. /* extracts lines of selected type, all other features are discarded */
  318. /* options where, cats, and layer are ignored */
  319. G_message(_("Network generalization..."));
  320. total_output =
  321. graph_generalization(&In, &Out, mask_type, degree_thresh,
  322. closeness_thresh, betweeness_thresh);
  323. }
  324. /* copy tables here because method == NETWORK is complete and
  325. * tables for Out may be needed for parse_filter_options() below */
  326. if (!notab_flag->answer) {
  327. if (method == NETWORK)
  328. copy_tables_by_cats(&In, &Out);
  329. else
  330. Vect_copy_tables(&In, &Out, -1);
  331. }
  332. else if (where_opt->answer && method < NETWORK) {
  333. G_warning(_("Attributes are needed for 'where' option, copying table"));
  334. Vect_copy_tables(&In, &Out, -1);
  335. }
  336. /* smoothing/simplification */
  337. if (method < NETWORK) {
  338. /* modifies only lines of selected type, all other features are preserved */
  339. int not_modified_boundaries = 0, n_oversimplified = 0;
  340. struct line_pnts *APoints; /* original Points */
  341. Vect_copy_map_lines(&In, &Out);
  342. Vect_build_partial(&Out, GV_BUILD_CENTROIDS);
  343. if ((mask_type & GV_AREA) && !(mask_type & GV_BOUNDARY))
  344. mask_type |= GV_BOUNDARY;
  345. G_message("-----------------------------------------------------");
  346. G_message(_("Generalization (%s)..."), method_opt->answer);
  347. G_message(_("Using threshold: %g %s"), thresh, G_database_unit_name(1));
  348. G_percent_reset();
  349. APoints = Vect_new_line_struct();
  350. n_lines = Vect_get_num_lines(&Out);
  351. for (i = 1; i <= n_lines; i++) {
  352. int after = 0;
  353. G_percent(i, n_lines, 1);
  354. type = Vect_read_line(&Out, APoints, Cats, i);
  355. if (!(type & GV_LINES) || !(mask_type & type))
  356. continue;
  357. if (layer > 0) {
  358. if ((type & GV_LINE) &&
  359. !Vect_cats_in_constraint(Cats, layer, cat_list))
  360. continue;
  361. else if ((type & GV_BOUNDARY)) {
  362. int do_line = 0;
  363. int left, right;
  364. do_line = Vect_cats_in_constraint(Cats, layer, cat_list);
  365. if (!do_line) {
  366. /* check if any of the centroids is selected */
  367. Vect_get_line_areas(&Out, i, &left, &right);
  368. if (left > 0) {
  369. Vect_get_area_cats(&Out, left, Cats);
  370. do_line = Vect_cats_in_constraint(Cats, layer, cat_list);
  371. }
  372. else if (left < 0) {
  373. left = Vect_get_isle_area(&Out, abs(left));
  374. if (left > 0) {
  375. Vect_get_area_cats(&Out, left, Cats);
  376. do_line = Vect_cats_in_constraint(Cats, layer, cat_list);
  377. }
  378. }
  379. if (!do_line) {
  380. if (right > 0) {
  381. Vect_get_area_cats(&Out, right, Cats);
  382. do_line = Vect_cats_in_constraint(Cats, layer, cat_list);
  383. }
  384. else if (right < 0) {
  385. right = Vect_get_isle_area(&Out, abs(right));
  386. if (right > 0) {
  387. Vect_get_area_cats(&Out, right, Cats);
  388. do_line = Vect_cats_in_constraint(Cats, layer, cat_list);
  389. }
  390. }
  391. }
  392. }
  393. if (!do_line)
  394. continue;
  395. }
  396. }
  397. Vect_line_prune(APoints);
  398. if (APoints->n_points < 2)
  399. /* Line of length zero, delete if boundary ? */
  400. continue;
  401. total_input += APoints->n_points;
  402. /* copy points */
  403. Vect_reset_line(Points);
  404. Vect_append_points(Points, APoints, GV_FORWARD);
  405. loop_support = 0;
  406. if (!loop_support_flag->answer) {
  407. int n1, n2;
  408. Vect_get_line_nodes(&Out, i, &n1, &n2);
  409. if (n1 == n2) {
  410. if (Vect_get_node_n_lines(&Out, n1) == 2) {
  411. if (abs(Vect_get_node_line(&Out, n1, 0)) == i &&
  412. abs(Vect_get_node_line(&Out, n1, 1)) == i)
  413. loop_support = 1;
  414. }
  415. }
  416. }
  417. for (iter = 0; iter < iterations; iter++) {
  418. switch (method) {
  419. case DOUGLAS:
  420. douglas_peucker(Points, thresh, with_z);
  421. break;
  422. case DOUGLAS_REDUCTION:
  423. douglas_peucker_reduction(Points, thresh, reduction,
  424. with_z);
  425. break;
  426. case LANG:
  427. lang(Points, thresh, look_ahead, with_z);
  428. break;
  429. case VERTEX_REDUCTION:
  430. vertex_reduction(Points, thresh, with_z);
  431. break;
  432. case REUMANN:
  433. reumann_witkam(Points, thresh, with_z);
  434. break;
  435. case BOYLE:
  436. boyle(Points, look_ahead, loop_support, with_z);
  437. break;
  438. case SLIDING_AVERAGING:
  439. sliding_averaging(Points, slide, look_ahead, loop_support, with_z);
  440. break;
  441. case DISTANCE_WEIGHTING:
  442. distance_weighting(Points, slide, look_ahead, loop_support, with_z);
  443. break;
  444. case CHAIKEN:
  445. chaiken(Points, thresh, loop_support, with_z);
  446. break;
  447. case HERMITE:
  448. hermite(Points, thresh, angle_thresh, loop_support, with_z);
  449. break;
  450. case SNAKES:
  451. snakes(Points, alpha, beta, loop_support, with_z);
  452. break;
  453. }
  454. }
  455. if (loop_support == 0) {
  456. /* safety check, BUG in method if not passed */
  457. if (APoints->x[0] != Points->x[0] ||
  458. APoints->y[0] != Points->y[0] ||
  459. APoints->z[0] != Points->z[0])
  460. G_fatal_error(_("Method '%s' did not preserve first point"), method_opt->answer);
  461. if (APoints->x[APoints->n_points - 1] != Points->x[Points->n_points - 1] ||
  462. APoints->y[APoints->n_points - 1] != Points->y[Points->n_points - 1] ||
  463. APoints->z[APoints->n_points - 1] != Points->z[Points->n_points - 1])
  464. G_fatal_error(_("Method '%s' did not preserve last point"), method_opt->answer);
  465. }
  466. else {
  467. /* safety check, BUG in method if not passed */
  468. if (Points->x[0] != Points->x[Points->n_points - 1] ||
  469. Points->y[0] != Points->y[Points->n_points - 1] ||
  470. Points->z[0] != Points->z[Points->n_points - 1])
  471. G_fatal_error(_("Method '%s' did not preserve loop"), method_opt->answer);
  472. }
  473. Vect_line_prune(Points);
  474. /* oversimplified line */
  475. if (Points->n_points < 2) {
  476. after = APoints->n_points;
  477. n_oversimplified++;
  478. if (error_out->answer)
  479. Vect_write_line(&Error, type, APoints, Cats);
  480. }
  481. /* check for topology corruption */
  482. else if (type == GV_BOUNDARY) {
  483. if (!check_topo(&Out, i, APoints, Points, Cats)) {
  484. after = APoints->n_points;
  485. not_modified_boundaries++;
  486. if (error_out->answer)
  487. Vect_write_line(&Error, type, APoints, Cats);
  488. }
  489. else
  490. after = Points->n_points;
  491. }
  492. else {
  493. /* type == GV_LINE */
  494. Vect_rewrite_line(&Out, i, type, Points, Cats);
  495. after = Points->n_points;
  496. }
  497. total_output += after;
  498. }
  499. if (not_modified_boundaries > 0)
  500. G_warning(_("%d boundaries were not modified because modification would damage topology"),
  501. not_modified_boundaries);
  502. if (n_oversimplified > 0)
  503. G_warning(_("%d lines/boundaries were not modified due to over-simplification"),
  504. n_oversimplified);
  505. G_message("-----------------------------------------------------");
  506. /* make sure that clean topo is built at the end */
  507. Vect_build_partial(&Out, GV_BUILD_NONE);
  508. if (error_out->answer)
  509. Vect_build_partial(&Error, GV_BUILD_NONE);
  510. }
  511. Vect_build(&Out);
  512. if (error_out->answer)
  513. Vect_build(&Error);
  514. Vect_close(&In);
  515. Vect_close(&Out);
  516. if (error_out->answer)
  517. Vect_close(&Error);
  518. G_message("-----------------------------------------------------");
  519. if (total_input != 0 && total_input != total_output)
  520. G_done_msg(_("Number of vertices for selected features %s from %d to %d (%d%% remaining)"),
  521. simplification ? _("reduced") : _("changed"),
  522. total_input, total_output,
  523. (total_output * 100) / total_input);
  524. else
  525. G_done_msg(" ");
  526. exit(EXIT_SUCCESS);
  527. }