source: subversion/applications/rendering/osmAtHome/lines2curves.pl @ 6704

Last change on this file since 6704 was 2194, checked in by jdschmidt, 13 years ago

updated with Osmarender4 and beziercurvehinting

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