source: subversion/rendering/osmarender4/lines2curves.pl @ 2486

Last change on this file since 2486 was 2247, checked in by enxrah, 13 years ago

roll back lines2curves.pl

  • Property svn:executable set to *
File size: 15.1 KB
Line 
1#!/usr/bin/perl -w
2#-----------------------------------------------------------------------------
3#
4#  lines2curves.pl
5#
6#  This script post-processes Osmarender output to change lines in ways
7#  into smooth bezier curves.
8#
9#  This is what it does for each 'way' in the svg:
10#     For each pair of lines that make up the way it will replace the two
11#     straight lines with an appropriate bezier curve through the point where
12#     the lines meet. Appropriate means that the curve is more localised the
13#     sharper the angle, and if the angle is less than 90 degrees it will not
14#     introduce a bezier curve.
15#
16#     If the point where the lines meet has other ways that meet it (at a 'T'
17#     junction for example), it leaves those segments untouched (i.e. it
18#     doesn't introduce curves for those segments).
19#
20#  Call it as follows:
21#
22#    lines2curves.pl YourSVGFile.svg >SVGFileWithCurves.svg
23#
24#-----------------------------------------------------------------------------
25#
26#  Copyright 2007 by Barry Crabtree
27#
28#  This program is free software; you can redistribute it and/or modify
29#  it under the terms of the GNU General Public License as published by
30#  the Free Software Foundation; either version 2 of the License, or
31#  (at your option) any later version.
32#
33#  This program is distributed in the hope that it will be useful,
34#  but WITHOUT ANY WARRANTY; without even the implied warranty of
35#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
36#  GNU General Public License for more details.
37#
38#  You should have received a copy of the GNU General Public License
39#  along with this program; if not, write to the Free Software
40#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA
41#
42#-----------------------------------------------------------------------------
43
44use strict;
45use Carp;
46use Math::Complex;
47use Math::Vec qw(:terse);
48
49my $min_angle = 0.5;
50my $min_scale = 0;
51#
52# transform linear paths to curves
53#
54# Globals...
55my $line_position = 0; # current line position in the svg file
56my @svg_lines = ();    # the lines from the svg file
57my %point_is_in;       # lookup for 'what ways is this point in?'
58my %to_transform;      # ways that need transforming
59
60# first pass, read in the svg lines, build the %point_is_in @svg_lines &
61# %to_transform structures.
62while (<>) {
63    my $line = $_;
64    $svg_lines[$line_position] = $line;
65    if ( $line =~ m{(<path \s id=\"(?:way|area)_\w+\" \s d=\") # the prefix of the path
66                    ([^\"Z]+)                          # the core path itself
67                    (.*/>)$                           # the rest
68                   }x ) { # found a path
69        my $path_prefix    = $1;
70        my $path_statement = $2;
71        my $path_suffix    = $3;
72        my $path_points_ref = path_points($path_statement);
73        foreach my $point (@$path_points_ref) {
74            my $point_index = $point->[0].$point->[1];
75            my $way_list_ref = $point_is_in{$point_index};
76            push @$way_list_ref, $path_prefix;
77            $point_is_in{$point_index} = $way_list_ref;
78        }
79
80        $to_transform{$path_prefix}
81            = [$line_position, $path_statement, $path_suffix, $path_points_ref];
82       
83    }
84    $line_position++;
85}
86
87# Second pass, create the bezier curve versions of all the paths that have
88# been found.
89foreach my $path (keys %to_transform) {
90    # transform path_statment to curves
91    my $path_info_ref  = $to_transform{$path};
92    my $line_index     = $path_info_ref->[0];
93    my $path_statement = $path_info_ref->[1];
94    my $path_suffix    = $path_info_ref->[2];
95
96    my $transformed_path = curvify_path($path_statement, $path);
97    $svg_lines[$line_index] = $path.$transformed_path.$path_suffix."\n";
98}
99
100# print out the transformed svg file.
101foreach my $line (@svg_lines) {
102    print $line;
103}
104
105
106# Get all the points in a path, removing duplicates along the way...
107sub path_points {
108    my $path_string = shift;
109    my $path_points_ref;
110
111    # there may be multiple moves in a path so get each one
112    my @move_segments = split /M\s*/, $path_string;
113
114    foreach my $move_segment (@move_segments) {
115        next if !$move_segment; # there is no pre-seg if there is only 1 'M'
116
117        # get all the points in the path
118        my $tmp_points_ref = [map [split /(?:\s|,)/, $_], split('L', $move_segment)];
119        # stop those occasional divide by zero errors...
120        push @$path_points_ref, @$tmp_points_ref; 
121    }
122
123    my $clean_points_ref = remove_duplicate_points($path_points_ref);
124    return $clean_points_ref;
125}
126
127sub remove_duplicate_points {
128    my $points_ref = shift;
129
130    my $clean_points_ref = [$points_ref->[0]];
131    shift @$points_ref;
132
133    foreach my $point_ref (@$points_ref) {
134        if ($point_ref->[0].$point_ref->[1] eq
135                $clean_points_ref->[-1][0].$clean_points_ref->[-1][1]) {
136                next;
137        }
138
139        push @$clean_points_ref, $point_ref;
140    }
141    if (scalar(@$points_ref)+1 != scalar(@$clean_points_ref)) {
142    } 
143    return $clean_points_ref;
144}
145
146sub remove_spur_points {
147    my $points_ref = shift;
148
149# spur points are when you get a way that goes A->B->A->C. The point 'B' is a
150# spur point & we don't like 'em.
151
152    my $clean_points_ref = [$points_ref->[0]];
153
154    shift @$points_ref;
155
156    for(my $i=0; $i < scalar(@$points_ref)-1; $i++) {
157        if ($clean_points_ref->[-1][0].$clean_points_ref->[-1][1] ne 
158            $points_ref->[$i+1][0].$points_ref->[$i+1][1]
159           ) {
160           push @$clean_points_ref, $points_ref->[$i];
161        }
162    }
163    push @$clean_points_ref, $points_ref->[-1];
164
165    return $clean_points_ref;
166}
167
168sub dup_points {
169    my $points_ref = shift;
170
171    foreach my $p (@$points_ref) {
172        my $count = 0;
173        foreach my $q (@$points_ref) {
174            if (join('', @$p) eq join('', @$q)) {
175                $count++;
176            }
177        }
178        if ($count > 1) {
179            return 1;
180        }
181    }
182    return;
183}
184
185# splits up the path string and calls 'from_lines_to_curves' appropriately to
186# build up a modified path string.
187sub curvify_path {
188    my $path_string = shift;
189    my $way_id      = shift;
190
191    my $tmp_string = $path_string;
192    $tmp_string =~ s/[^L]//g;
193
194    #
195    if (length($tmp_string) < 1) { # cant do much with a single line segment
196        return $path_string;
197    }
198
199    my $bezier_path_string = q{};
200
201    # there may be multiple moves in a path so get each one
202    my @move_segments = split /M\s*/, $path_string;
203
204    foreach my $move_segment (@move_segments) {
205        my $postfix = q{};
206        next if !$move_segment; # there is no pre-seg if there is only 1 'M'
207
208        if ($move_segment =~ s/Z$//) {
209            $postfix = 'Z';
210        }
211        # get all the points in the path
212        my @path_points = map [split /(?:\s|,)/, $_], split('L', $move_segment);
213
214        if ($way_id =~ /way_/ && dup_points(\@path_points)) {
215            $bezier_path_string .= "M$path_string$postfix"; 
216        } else {
217            $bezier_path_string 
218                .= 'M'.from_lines_to_curves(\@path_points, $way_id).$postfix;
219        }
220    }
221
222    return $bezier_path_string;
223}
224
225# When two ways meet at their ends, we need the second point in the 'other'
226# way to get good control points in the bezier curve. This just returns the
227# second point.
228sub get_second_point {
229    my ($start_point, $way_id) = @_;
230
231# uncomment the line below if you don't want the last segment in a way to be
232# curved when it meets another way.
233#
234#    return undef;
235    my $ways_ref = $point_is_in{$start_point};
236
237    if ($way_id =~ /area_/) { # areas are easier...
238        my $way_points = $to_transform{$way_id}->[3];
239        if ($way_points->[0][0].$way_points->[0][1] eq $start_point) {
240            return $way_points->[-1];
241        } else {
242            return $way_points->[0];
243        }
244    }
245
246    # now do normal ways...
247    # more than two ways meet - dont curve these.
248    return undef if @$ways_ref != 2;
249
250    my $otherway = ($ways_ref->[0] eq $way_id && $ways_ref->[1] ne $way_id)? $ways_ref->[1]: $ways_ref->[0];
251
252    # maybe there wasn't another way.
253    return undef if !$otherway;
254    # maybe this way has a loop in it.
255#    return undef if $otherway eq $way_id;
256
257    my $way_points = $to_transform{$otherway}->[3];
258   
259    if ($start_point eq $way_points->[0][0].$way_points->[0][1]) {
260        return $way_points->[1];
261    } elsif ($start_point eq $way_points->[-1][0].$way_points->[-1][1]) {
262        return $way_points->[-2];
263    }
264
265    return undef;
266}
267
268# heavy work here. Takes the set of points in a path and generates a bezier
269# version where appropriate.
270sub from_lines_to_curves {
271    my $points_ref = shift;
272    my $way_id     = shift;
273
274    my $cp_range = 0.5;
275    my $incremental_string = q{};
276
277    # Add a point at either end of the set of points to make it easy to
278    # generate the correct control points. If this way is standalone, then the
279    # ends of the way are extended straight back from the start/end
280    # points. If it joins another way, then use the second point in the
281    # other way as the end point. This will make ways that join connect
282    # smoothly - a point that Jochn Topf noticed.
283    #
284    my $start_point = $points_ref->[0][0].$points_ref->[0][1];
285    my $end_point   = $points_ref->[-1][0].$points_ref->[-1][1];
286
287    my $second_point_ref;
288   
289    if ($start_point eq $end_point && $way_id =~ /area_/) {
290        $second_point_ref = $points_ref->[-2];
291    } else {
292        $second_point_ref = get_second_point($start_point, $way_id);
293    }
294    if ($second_point_ref && $second_point_ref->[0].$second_point_ref->[1] ne
295    $points_ref->[1][0].$points_ref->[1][1]) {
296        unshift @$points_ref, $second_point_ref;
297    } else { # make a dummy point
298        unshift @$points_ref, [ $points_ref->[0][0]
299                               -$points_ref->[1][0] + $points_ref->[0][0],
300                                $points_ref->[0][1]
301                               -$points_ref->[1][1] + $points_ref->[0][1] ];
302    }
303
304    if ($start_point eq $end_point && $way_id =~ /area_/) {
305        $second_point_ref = $points_ref->[1];
306    } else {
307        $second_point_ref = get_second_point($end_point, $way_id);
308    }
309    if ($second_point_ref && $second_point_ref->[0].$second_point_ref->[1] ne
310    $points_ref->[-2][0].$points_ref->[-2][1]) {
311        push @$points_ref, $second_point_ref;
312    } else { # make a dummy point
313        push @$points_ref, [ $points_ref->[-1][0]
314                            +$points_ref->[-1][0] - $points_ref->[-2][0],
315                             $points_ref->[-1][1]
316                            +$points_ref->[-1][1] - $points_ref->[-2][1] ];
317    }
318
319    $points_ref = remove_duplicate_points(remove_spur_points($points_ref));
320    my $points_in_line     = scalar(@$points_ref);
321    my $current_point      = 0;
322    my $path_start_ref     = shift @$points_ref;
323    my $path_mid_ref       = $points_ref->[0];
324    my $path_end_ref       = $points_ref->[1];
325
326    my ($start_v,  $mid_v,  $end_v, $mid_start_v, $mid_end_v );
327    my ($start_mid_nv, $mid_start_nv, $mid_end_nv);
328    my $control_v;
329    my $control_scale;
330
331    my $pl;
332
333    foreach my $p (@$points_ref) {
334        $pl .= "\n\t".join(':', @$p);
335    }
336#    print "$way_id: $pl\n";
337
338    # go round each set of 3 points to generate the bezier points
339    while (@$points_ref >= 3) {
340        if (!$incremental_string) {
341            $incremental_string = q{ }.$path_mid_ref->[0].q{ }.$path_mid_ref->[1];
342        }
343
344        # decide to use real control points or not. We only use real control
345        # points if this node is only referenced in one 'way' or we are at the
346        # beginning/end of a way that only joins with one other way. This makes ways
347        # that 'T' sharp on the join.
348        if ( $path_start_ref->[0].$path_start_ref->[1] ne
349             $path_end_ref->[0].$path_end_ref->[1] &&
350             $path_mid_ref->[0].$path_mid_ref->[1] ne
351             $points_ref->[2][0].$points_ref->[2][1]
352             && ($way_id =~ /area_/
353            || @{$point_is_in{$path_mid_ref->[0].$path_mid_ref->[1]}} == 1
354            || (   $current_point == 0 
355                && @{$point_is_in{$path_mid_ref->[0].$path_mid_ref->[1]}} == 2)
356            || (   $current_point == $points_in_line-3 
357                && @{$point_is_in{$path_mid_ref->[0].$path_mid_ref->[1]}} == 2)
358            )) {
359
360#           print "\n\t->".join(':', @$path_start_ref)." ".join(':',
361#            @$path_mid_ref)." ".join(':',@$path_end_ref)."\n";
362            $incremental_string .= ' C ';
363            # work out control point 1 from $path_start, $path_mid & $path_end
364            $start_v = V($path_start_ref->[0], $path_start_ref->[1]);
365            $mid_v   = V($path_mid_ref->[0], $path_mid_ref->[1]);
366            $end_v   = V($path_end_ref->[0], $path_end_ref->[1]);
367            $start_mid_nv = U( ($mid_v-$start_v) + ($end_v-$mid_v) );
368            $mid_start_v = V($start_v->[0]-$mid_v->[0], $start_v->[1]-$mid_v->[1]);
369            $mid_end_v   = V($end_v->[0]-$mid_v->[0], $end_v->[1]-$mid_v->[1]);
370            $control_scale = normalise_cp($mid_start_v, $mid_end_v);
371            $control_v = $mid_v + V($start_mid_nv->ScalarMult($control_scale*abs($end_v-$mid_v)*$cp_range));
372
373            $incremental_string .= $control_v->[0].','.$control_v->[1].q{ };
374           
375            # move on a segment
376            $path_start_ref = $path_mid_ref;
377            $path_mid_ref   = $path_end_ref;
378            $path_end_ref   = $points_ref->[2];
379            shift @$points_ref;
380   
381#           print "\n\t-->".join(':', @$path_start_ref)." ".join(':',
382#            @$path_mid_ref)." ".join(':',@$path_end_ref)."\n";
383            # work out control point 2 from new $path_start, $path_mid & path_end
384            $start_v = V($path_start_ref->[0], $path_start_ref->[1]);
385            $mid_v   = V($path_mid_ref->[0], $path_mid_ref->[1]);
386            $end_v   = V($path_end_ref->[0], $path_end_ref->[1]);
387            $start_mid_nv = U( ($start_v-$mid_v) + ($mid_v-$end_v) );
388            $mid_start_v = V($start_v->[0]-$mid_v->[0], $start_v->[1]-$mid_v->[1]);
389            $mid_end_v   = V($end_v->[0]-$mid_v->[0], $end_v->[1]-$mid_v->[1]);
390                $control_scale = normalise_cp($mid_start_v, $mid_end_v);
391            $control_v = $mid_v + V($start_mid_nv->ScalarMult($control_scale * abs($mid_v-$start_v)*$cp_range));
392   
393            $incremental_string .= $control_v->[0].','.$control_v->[1].q{ };
394   
395            $incremental_string .= $path_mid_ref->[0].q{ }.$path_mid_ref->[1].q{ };
396        } else { # make a straight line segment
397            $incremental_string .= 'L'.$path_end_ref->[0].','.$path_end_ref->[1].q{};
398            # move on a segment
399            $path_start_ref = $path_mid_ref;
400            $path_mid_ref   = $path_end_ref;
401            $path_end_ref   = $points_ref->[2];
402            shift @$points_ref; 
403        }
404    }
405   
406    return $incremental_string;
407}
408
409
410# if the angle of the control point is less than 90 degrees return 0
411# between 90 & 180 degrees return a number between 0 & 1.
412sub normalise_cp {
413    my ($start_v, $end_v) = @_;
414    my $PI = 3.1415926;
415    my $max_angle = $PI/2; # 180degrees
416    my $angle = $start_v->InnerAngle($end_v);
417
418    $angle = Re($angle);
419   
420    if ($angle < $PI*$min_angle) { # too small, so
421        return 0;
422    }
423
424    # angle is between $PI/4 and $PI/2
425    $angle = $angle - $PI*$min_angle;
426    return $angle / ($PI*$min_angle);
427}
Note: See TracBrowser for help on using the repository browser.