source: subversion/applications/utils/sanitize/sanitize.pl @ 4320

Last change on this file since 4320 was 1545, checked in by joerg, 14 years ago

insert Man pages/Copyright Info/Copright? Stubs to some of our perl Programs

  • Property svn:executable set to *
File size: 18.3 KB
Line 
1#!/usr/bin/perl
2# Copyright (C) 2006, and GNU GPL'd, by Joerg Ostertag
3
4my $VERSION ="check_osm.pl (c) Joerg Ostertag
5Initial Version (Sept,2006) by Joerg Ostertag <sanitize-openstreetmap\@ostertag.name>
6Version 0.01
7";
8
9BEGIN {
10    my $dir = $0;
11    $dir =~s,[^/]+/[^/]+$,,;
12    unshift(@INC,"$dir/perl");
13
14    unshift(@INC,"../perl");
15    unshift(@INC,"~/svn.openstreetmap.org/utils/perl");
16    unshift(@INC,"$ENV{HOME}/svn.openstreetmap.org/utils/perl");
17}
18
19
20use strict;
21use warnings;
22
23use File::Basename;
24use File::Copy;
25use File::Path;
26use Getopt::Long;
27use IO::File;
28use POSIX qw(ceil floor);
29use Pod::Usage;
30
31use Utils::File;
32use Utils::Math;
33use Utils::Debug;
34use Utils::LWP::Utils;
35use Geo::OSM::Write;
36use Data::Dumper;
37use XML::Parser;
38
39sub read_osm_file($); # {}
40sub check_Data(); #{ }
41
42my ($man,$help);
43
44our ($lat_min,$lat_max,$lon_min,$lon_max) = (0,0,0,0);
45
46our $osm_file ='-';     # The complete osm Filename (including path) for reading
47our $osm_out_file ="-"; # if needed, the pure filename of the osm output File
48our $do_redeem_lonesome_nodes = 0;
49
50our $OSM;
51
52# Set defaults and get options from command line
53Getopt::Long::Configure('no_ignore_case');
54GetOptions ( 
55             'debug+'        => \$DEBUG,     
56             'd+'            => \$DEBUG,     
57             'verbose+'      => \$VERBOSE,
58             'MAN'           => \$man, 
59             'man'           => \$man, 
60             'h|help|x'      => \$help, 
61
62             'osm-file=s'    => \$osm_file,
63             'osm-out-file'  => \$osm_out_file,
64             'redeem-lonesome-nodes' => \$do_redeem_lonesome_nodes,
65             )
66    or pod2usage(1);
67
68pod2usage(1) if $help;
69pod2usage(-verbose=>2) if $man;
70
71if ( $VERBOSE || $DEBUG ) {
72    print STDERR "\nChecking\n";
73}
74# Empty out global Variables for next loop
75$OSM->{nodes}    = {};
76$OSM->{segments} = {};
77$OSM->{ways}     = {};
78$OSM->{tool} = "OpenStreetMap Sanitizer";
79#$OSM->{tool} = "JOSM";
80
81my $start_time=time();
82
83read_osm_file( $osm_file );
84
85#  check openstreetmap data in Memory
86check_Data();
87
88#print STDERR Dumper(\$OSM);
89Geo::OSM::Write::write_osm_file($osm_out_file,$OSM);
90
91if ( $VERBOSE || $DEBUG ) {
92    printf STDERR "Check Completed for $osm_file in %.0f sec\n\n",time()-$start_time;
93}
94
95
96exit 0;
97
98##################################################################
99# Functions
100##################################################################
101
102our ($MainAttr,$Tags, $WaySegments);
103
104# Function is called whenever an XML tag is started
105#----------------------------------------------
106sub DoStart() {
107    my ($expat, $name, %attr) = @_;
108   
109    if($name eq "node"){
110        $Tags={};
111        %{$MainAttr} = %attr;
112    }
113    if($name eq "segment"){
114        $Tags={};
115        %{$MainAttr} = %attr;
116    }
117    if($name eq "way"){
118        $Tags={};
119        $WaySegments=[];
120        %{$MainAttr} = %attr;
121    }
122    if($name eq "tag"){
123        # TODO: protect against id,from,to,lat,long,etc. being used as tags
124        $Tags->{$attr{"k"}} = $attr{"v"};
125    }
126    if($name eq "seg"){
127        my $id = $attr{"id"};
128        push(@{$WaySegments}, $id);
129    }
130}
131
132# Function is called whenever an XML tag is ended
133#----------------------------------------------
134sub DoEnd(){
135    my ($expat, $element) = @_;
136    my $id = $MainAttr->{"id"};
137    if($element eq "node"){
138        my $node={};
139        %{$node} = %{$MainAttr};
140        $node->{tag}=$Tags;
141        $OSM->{nodes}->{$id} = $node;
142        #print "Node:".join(",",keys(%{$Tags}))."\n" if(scalar(keys(%{$Tags}))>0);
143    }
144
145    if($element eq "segment"){
146        my $segment={};
147        %{$segment} = %{$MainAttr};
148        $segment->{tag} = $Tags;
149        $OSM->{segments}->{$id}=$segment;
150    }
151
152    if($element eq "way"){
153        my $way={};
154        $way->{seg} = $WaySegments;
155        $way->{tag} = $Tags;
156        $OSM->{ways}->{$id} = $way;
157    }
158
159}
160
161# Function is called whenever text is encountered in the XML file
162#----------------------------------------------
163sub DoChar(){
164    my ($expat, $String) = @_;
165}
166
167############################################
168# -----------------------------------------------------------------------------
169sub read_osm_file($) { # Insert Streets from osm File
170    my $file_name = shift;
171
172    my $start_time=time();
173
174    print "Read OSM Data\n" if $VERBOSE || $DEBUG;
175
176    print STDERR "Parsing file: $file_name\n" if $DEBUG;
177    my $p = XML::Parser->new( Handlers => {
178        Start => \&DoStart, 
179        End => \&DoEnd, 
180        Char => \&DoChar},
181                              ErrorContext => 10,
182                              );
183   
184    my $fh = data_open($file_name);
185    die "Cannot open OSM File $file_name\n" unless $fh;
186    eval {
187        $p->parse($fh);
188    };
189    print "\n" if $DEBUG || $VERBOSE;
190    if ( $VERBOSE) {
191        printf "Read and parsed $file_name in %.0f sec\n",time()-$start_time;
192    }
193    if ( $@ ) {
194        warn "$@Error while parsing\n $file_name\n";
195        return;
196    }
197    if (not $p) {
198        warn "WARNING: Could not parse osm data\n";
199        return;
200    }
201
202
203    return;
204}
205
206
207# Distance between 2 points
208sub p2p_distance($$){
209    my $p1 = shift;
210    my $p2 = shift;
211    my $delta_lat=abs( $p1->{lat} - $p2->{lat} );
212    my $delta_lon=abs( $p1->{lon} - $p2->{lon} );
213    my $dist = sqrt($delta_lat*$delta_lat+$delta_lon*$delta_lon)*40000/360;
214    return $dist;
215}
216
217
218sub adjust_bounding_box($$$){
219    my $bbox = shift;
220    my $lat = shift;
221    my $lon = shift;
222
223    for my $type ( qw(lat_min lat_max lon_min lon_max lat lon )) {
224        next if defined ($bbox->{$type});
225        if ( $type =~m/min/ ) {
226            $bbox->{$type} = 1000;
227        } else {
228            $bbox->{$type} = -1000;
229        }
230    }
231    # remember lat/lon Min/Max
232    $bbox->{lat_min}= min($bbox->{lat_min},$lat);
233    $bbox->{lat_max}= max($bbox->{lat_max},$lat);
234    $bbox->{lon_min}= min($bbox->{lon_min},$lon);
235    $bbox->{lon_max}= max($bbox->{lon_max},$lon);
236    $bbox->{lat}= ($bbox->{lat_min}+$bbox->{lat_max})/2;
237    $bbox->{lon}= ($bbox->{lon_min}+$bbox->{lon_max})/2;
238}
239
240sub adjust_bounding_box_for_segment($$){
241    my $bbox = shift;
242    my $segment = shift;
243    $segment = $OSM->{segments}->{$segment}
244    if $segment =~ m/^\d+$/;
245    my $node_from = $segment->{from};
246    my $node_to   = $segment->{to};
247    my $lat1 = $OSM->{nodes}->{$node_from}->{lat};
248    my $lon1 = $OSM->{nodes}->{$node_from}->{lon};
249    my $lat2 = $OSM->{nodes}->{$node_to}->{lat};
250    my $lon2 = $OSM->{nodes}->{$node_to}->{lon};
251    adjust_bounding_box($bbox,$lat1,$lon1);
252    adjust_bounding_box($bbox,$lat2,$lon2);
253}
254
255# ------------------------------------------------------------------
256sub tag_string($){
257    my $obj = shift;
258    my $tags      = $obj->{tag};
259    my $tags_string = join(" ",map{" $_:$tags->{$_} "}keys %$tags);
260    my $erg ='';
261    $erg = " Tags[$tags_string] " if $tags_string;
262    return $erg;
263}
264
265# ------------------------------------------------------------------
266# Create Link to a Node/segment/way
267sub link_to_obj($){
268    my $obj = shift;
269
270    my $bbox = { lat_min => 1000, lat_max => -1000,
271                 lon_min => 1000, lon_max => -1000};
272
273    my $type = "unknown type ";
274    my $obj_txt = "";
275    if ( defined($obj->{lat})) {          # Node
276        $type = "Node";
277        $obj_txt = "Node ".$obj->{id}." (".$obj->{lat}.",".$obj->{lon}.")";
278        adjust_bounding_box($bbox,$obj->{lat},$obj->{lon});
279    } elsif ( defined($obj->{from})) {    # Segment
280        $type = "Segment";
281        $obj_txt = " ";
282        #Segment ".$obj->{id}." (Node ".$obj->{from}." --> Node ".$obj->{to}.")";
283        adjust_bounding_box_for_segment($bbox,$obj);
284    } elsif ( defined($obj->{seg})) {     # Way
285        $type = "Way";
286        $obj_txt = "Way: $obj->{id}<br>\n";
287        $obj_txt .= "Segments[";
288        for my $seg_id ( @{$obj->{seg}} ) {
289            $obj_txt .= "$seg_id,";
290            next unless $seg_id;
291            next unless defined $OSM->{segments}->{$seg_id};
292            adjust_bounding_box_for_segment($bbox,$seg_id);
293        }
294        $obj_txt .= "]";
295    } else {
296        print "link_to_obj: unrecognized type for  ".Dumper(\$obj);
297    };
298
299    my $erg='';
300    my $lat = ($bbox->{lat_min} + $bbox->{lat_max})/2;
301    my $lon = ($bbox->{lon_min} + $bbox->{lon_max})/2;
302    my $osm_link = "<A target=\"map\" href=\"http://www.openstreetmap.org/index.html?";
303
304    if ( $obj_txt ){
305        $erg .= $obj_txt;
306    } else {
307        $erg .= "$type: ".Dumper(\$obj);
308    };
309    $erg .= "<font size=-4>".tag_string($obj)."</font>";
310    $erg .= "\n";
311    if ( $type ne "Node" ) {
312        $erg .= "<td>".link_download_josm($bbox). "</td>";
313        #$erg .= "Bounding Box:\n";
314        #$erg .= sprintf( "(%6.5f,%6.5f) "  ,$bbox->{lat_min},$bbox->{lon_min});
315        #$erg .= sprintf( "(%6.5f,%6.5f)\n" ,$bbox->{lat_max},$bbox->{lon_max});
316    }
317
318
319    $erg .= "<td>";
320    $erg .= "Map($type) with Zoom: ";
321    for my $zoom ( qw( 10 15 20 24 )) {
322        $erg .= $osm_link . "lat=$lat&lon=$lon&zoom=$zoom\">$zoom</a> ";
323    }
324    $erg .= "</td>";
325    return $erg;
326}
327
328# ------------------------------------------------------------------
329# Link to OSM view of Node
330sub link_node($){
331    my $node = shift;
332    $node = $OSM->{nodes}->{$node} if $node =~ m/^\d+$/;
333
334    my $osm_link = "<A target=\"map\" href=\"http://www.openstreetmap.org/index.html?";
335    return $osm_link . "lat=$node->{lat}&lon=$node->{lon}&zoom=23\">";
336}
337
338sub link_download_josm($){
339    my $bbox = shift;
340    my $box = "$bbox->{lat_min},$bbox->{lon_min},$bbox->{lat_max},$bbox->{lon_max}";
341    my $erg .= "<font size=-3>josm&nbsp;--download=";
342    $erg .= sprintf( "%6.5f,%6.5f,"  ,$bbox->{lat_min}-0.001,$bbox->{lon_min}+0.001);
343    $erg .= sprintf( "%6.5f,%6.5f\n" ,$bbox->{lat_max}-0.001,$bbox->{lon_max}+0.001);
344    $erg .= "</font>";
345    return $erg; 
346}
347
348# ------------------------------------------------------------------
349sub check_osm_segments() { # Insert Streets from osm variables into mysql-db for gpsdrive
350    my $start_time=time();
351
352    if ( $VERBOSE || $DEBUG ) {
353        print STDERR "------------ Check Segments\n";
354    }
355
356    my $max_dist=0;
357    my $sum_dist=0;
358    my $min_dist=99999999999999;
359
360    my $counted_segmtnts=0;
361    for my $seg_id (  keys %{$OSM->{segments}} ) {
362        my $segment = $OSM->{segments}->{$seg_id};
363        my $node_from = $segment->{from};
364        my $node_to   = $segment->{to};
365
366
367        print "Undefined Node From for Segment $seg_id:".Dumper(\$segment)."\n"
368            unless $node_from;
369        print "Undefined Node To   for Segment $seg_id:".Dumper(\$segment)."\n"
370            unless $node_to;
371        $OSM->{nodes}->{$node_from}->{connections}++;
372        $OSM->{nodes}->{$node_to}->{connections}++;
373
374        my $lat1 = $OSM->{nodes}->{$node_from}->{lat};
375        my $lon1 = $OSM->{nodes}->{$node_from}->{lon};
376        my $lat2 = $OSM->{nodes}->{$node_to}->{lat};
377        my $lon2 = $OSM->{nodes}->{$node_to}->{lon};
378        my $dist = p2p_distance($OSM->{nodes}->{$node_from},$OSM->{nodes}->{$node_to});
379        $segment->{tag}->{distance}=$dist;
380
381        if ( $node_from &&  $node_to && 
382             $node_from == $node_to ) {
383            $segment->{tag}->{error} .= "from == to";
384            next;
385        }
386
387        if ( $lat1 == $lat2 && $lon1 == $lon2 ) {
388            $segment->{tag}->{error} .= "no distance";
389            next;
390        } 
391       
392        if ( $dist > 8 ) {
393            $segment->{tag}->{error} .= "long distance $dist Km,";
394        } 
395        my $dist_meters = $dist*1000;
396        if ( $dist_meters < 10.1 ) {
397            $segment->{tag}->{error} .= sprintf("small distance %.2f Km,",$dist);
398        } 
399    }
400
401    if ( $VERBOSE) {
402        printf "Checked OSM Segments in %.0f sec\n",time()-$start_time;
403    }
404}
405
406# ----------------------------------------------------------------------------------------
407sub check_osm_nodes() { 
408    my $start_time=time();
409   
410    if ( $VERBOSE || $DEBUG ) {
411        print STDERR "------------ Check Nodes\n";
412    }
413    my $node_positions={};
414    for my $node_id (  keys %{$OSM->{nodes}} ) {
415        my $node = $OSM->{nodes}->{$node_id};
416        my $lat = $node->{lat};
417        my $lon = $node->{lon};
418        push(@{$node_positions->{"$lat,$lon"}},$node_id);
419        if (abs($lat)>90) {
420            print "Node $node_id: lat: $lat out of range\n";
421        }
422        if (abs($lon)>180) {
423            print "Node $node_id: lon: $lon out of range\n";
424        }
425        if ( !$node->{connections} ) {
426            my $tag_string = tag_string($node);
427            $tag_string =~ s/Tags\[\s*//g;
428            $tag_string =~ s/\s*\]\s*//g;
429            $tag_string =~ s/class:(node|trackpoint)//g;
430            $tag_string =~ s/created_by:JOSM//g;
431            $tag_string =~ s/converted_by:Track2osm//g;
432            $tag_string =~ s/editor:osmpedit-svn//g;
433            $tag_string =~ s/editor:osmpedit//g;
434            $tag_string =~ s/\s*//g;
435            if ($tag_string =~ m/class:viewpoint/ ) {
436            } elsif ($tag_string =~ m/class:village/ ) {
437            } elsif ($tag_string =~ m/class:point of interest/ ) {
438            } elsif ($tag_string) {
439                if (0){
440                    print "Node $node_id is not connected, but has Tag: ";
441                    print tag_string($node)." ";
442                    print " ($tag_string) ";
443                    print "\n";
444                }
445            } else {
446                $node->{tag}->{error} .= "node-no-connections";
447                if ( $do_redeem_lonesome_nodes ) {
448                    delete $OSM->{nodes}->{$node_id};
449                }
450            }
451        }
452    }
453
454    my $count;
455    for my $position ( keys %{$node_positions} ) {
456        my $nodes = $node_positions->{$position};
457        my $node0=$OSM->{nodes}->{$nodes->[0]};
458        my $node_lat = $node0->{lat};
459        my $node_lon = $node0->{lon};
460        my $lat = floor($node_lat/5)*5;
461        my $lon = floor($node_lon/5)*5;
462        $count->{"node-$lat-$lon"}->{points}++;
463        if ( @{$nodes} > 1 ) {
464            $count->{"node-$lat-$lon"}->{err}++;
465            my $link = link_node($node0);
466            #html_out("node","<td>");
467            for my $node ( @{$nodes} ) {
468                #xml_out( "node-duplicate",$node);
469                #html_out("node",
470                #sprintf( "%s ( %d Seg, %s )<br>", $node, $OSM->{nodes}->{$node}->{connections}||0 ,
471                #tag_string($OSM->{nodes}->{$node})
472                #));
473            }
474            #html_out("node","</td></tr>");
475        }
476    }
477
478    if ( $VERBOSE) {
479        printf "Checked OSM Segments in %.0f sec\n",time()-$start_time;
480    }
481}
482
483# ----------------------------------------------------------------------------------------
484sub check_osm_ways() { 
485    my $start_time=time();
486
487    if ( $VERBOSE || $DEBUG ) {
488        print STDERR "------------ Check Ways\n";
489    }
490
491    # ---------------------- Undefined segments in way
492    for my $way_id ( keys %{$OSM->{ways}} ) {
493        my $way = $OSM->{ways}->{$way_id};
494       
495        for my $seg_id ( @{$way->{seg}} ) {
496            if ( ! defined $OSM->{segments}->{$seg_id} ){
497                $way->{tag}->{error} .= "way-undef-segment $seg_id";
498                next;
499            }
500            $OSM->{segments}->{$seg_id}->{referenced_by_way} ++;
501        }
502    }
503
504    # ---------------------- No Segments in way
505    my $segments_per_way={};
506    my $distance_per_way={};
507    #html_out("way","<h2>No Segments in way</h2>\n<ul>");
508    my $way_tags={};
509    for my $way_id (  keys %{$OSM->{ways}} ) {
510        my $way = $OSM->{ways}->{$way_id};
511
512        for my $k ( keys %{$way->{tag}} ) {
513            $way_tags->{$k}->{count}++;
514            $way_tags->{$k}->{values}->{$way->{tag}->{$k}} ++;
515        }
516
517        if ( ! defined ( $way->{seg} )) {
518            #html_out("way","<li>Way  $way_id has no segments\n".
519            #link_to_obj($way)
520            #);
521            next;
522        }
523        if ( ! defined ( $way->{tag} )) {
524            #html_out("way-no-tags","<li>Way  $way_id has no tags\n".
525            #link_to_obj($way)
526            #);
527        } else {
528            my $tag=$way->{tag};
529            if ( ! defined ( $tag->{name} )) {
530                #html_out("way-no-name","<li>Way $way_id has no name\n".
531                #link_to_obj($way)
532                #);
533            }
534        }
535        my $count = scalar @{$way->{seg}};
536        $count = int($count/10)*10;
537        #$count = "$count - ".($count+9);
538        $segments_per_way->{$count} ++;
539
540        my $distance=0;
541        for my $seg_id ( @{$way->{seg}} ) {
542            next unless defined ($OSM->{segments}->{$seg_id});
543            $distance +=  ($OSM->{segments}->{$seg_id}->{distance}||0);
544        }
545       
546        $distance = int($distance/10)*10;
547        #$distance = "$distance - ".($distance+9);
548        $distance_per_way->{$distance}++;
549    }
550    #html_out("way","</ul>");
551
552    # --------------------------
553    #html_out("statistics-ways", "<h3>Number of Segments in Ways</h3>");
554    #html_out("statistics-ways", "<table>");
555    #html_out("statistics-ways", "<tr><th></th> <th></th> </tr>");
556    for my $number_of_segments ( sort { $a <=> $b }  keys %{$segments_per_way} ) {
557        my $number_of_ways = $segments_per_way->{$number_of_segments};
558        #html_out("statistics-ways", "<tr>");
559        #html_out("statistics-ways", "<td>"."$number_of_ways  </td><td>ways with </td>");
560        #html_out("statistics-ways", "<td>$number_of_segments - ".($number_of_segments+9)." </td><td> Segments </td>\n");
561        #html_out("statistics-ways", "<tr>");
562    }
563    #html_out("statistics-ways", "</table>");
564
565}
566
567
568
569
570# *****************************************************************************
571sub check_Data(){
572    # Checks and statistics
573    check_osm_segments();
574    check_osm_nodes();
575    check_osm_ways();
576
577}
578
579
580##################################################################
581# Usage/manual
582
583__END__
584
585=head1 NAME
586
587B<check_osm.pl> Version 0.01
588
589=head1 DESCRIPTION
590
591B<sanitize.pl> is a program to check osm data and make sanitizing suggestions.
592Data from Openstreetmap
593
594This Programm is completely experimental, but for some cases it
595can already be used.
596
597So: Have Fun, improve it and send me fixes :-))
598
599=head1 SYNOPSIS
600
601B<Common usages:>
602
603sanitize.pl [-d] [-v] [-h] [--redeem-lonesome-nodes] [--osm-file=file.osm]  [--osm-out-file=out-file.osm]
604
605=head1 OPTIONS
606
607=over 2
608
609=item B<--man> Complete documentation
610
611Complete documentation
612
613=item B<--osm-file=path/data.osm>
614
615Select the "path/data.osm" file to use for the checks
616
617=item B<--redeem-lonesome-nodes>
618
619Redeem lonesome Nodes. This means, if the node is not
620connected to anywhere and has no tags it is deleted.
621
622=item B<--osm-file=file.osm>
623
624Input Filename. Default is standardin.
625
626=item B<--osm-out-file=out-file.osm>
627
628Output Filename. Default is standardout.
629
630=head1 JOSM
631
632I you put a File named ~/.josm/external_tools with the following
633content you can access it with the tools menu inside josm.
634
635 <tools>
636 <group name="sanitizer">
637  <tool
638    name="Tweety's OSM sanitizer (All)"
639    exec="/usr/bin/perl /home/tweety/bin/sanitize.pl"
640    in="selection"
641    flags="include_references,include_backreferences"
642    out="replace">
643  </tool>
644  <tool
645    name="Tweety's OSM Sanitizer (Selection)"
646    exec="/usr/bin/perl /home/tweety/bin/sanitize.pl"
647    in="selection"
648    flags="include_references,include_backreferences"
649    out="replace">
650  </tool>
651  <tool
652    name="Tweety's OSM Sanitizer (Selection,redeem-lonesome-nodes)"
653    exec="/usr/bin/perl /home/tweety/bin/sanitize.pl --redeem-lonesome-nodes"
654    in="selection"
655    flags="include_references,include_backreferences"
656    out="replace">
657  </tool>
658 </group>
659 </tools>
660=back
661
662
663=head1 COPYRIGHT
664
665Copyright 2006, Jörg Ostertag
666
667This program is free software; you can redistribute it and/or
668modify it under the terms of the GNU General Public License
669as published by the Free Software Foundation; either version 2
670of the License, or (at your option) any later version.
671
672This program is distributed in the hope that it will be useful,
673but WITHOUT ANY WARRANTY; without even the implied warranty of
674MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
675GNU General Public License for more details.
676
677You should have received a copy of the GNU General Public License
678along with this program; if not, write to the Free Software
679Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
680
681=head1 AUTHOR
682
683Jörg Ostertag (sanitize-for-openstreetmap@ostertag.name)
684
685=head1 SEE ALSO
686
687http://www.openstreetmap.org/
688
689=cut
Note: See TracBrowser for help on using the repository browser.