main.c 19 KB

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