source: subversion/applications/rendering/osmarender4/osm-svg-beautifier.pl @ 2731

Last change on this file since 2731 was 2312, checked in by enxrah, 13 years ago

add commments about the purpose of osm-svg-beautifier

  • Property svn:executable set to *
File size: 18.6 KB
Line 
1#!/usr/bin/perl -w
2#-----------------------------------------------------------------------------
3#
4#  osm-svg-beautifier.pl
5#
6#  This script post-processes Osmarender output to change lines in ways
7#  into smooth bezier curves, and to abbreviate (or completely remove)
8#  street names that don't fit onto their street.
9#
10#  This is what it does for each 'way' in the svg:
11#     For each pair of lines that make up the way it will replace the two
12#     straight lines with an appropriate bezier curve through the point where
13#     the lines meet. Appropriate means that the curve is more localised the
14#     sharper the angle, and if the angle is less than 90 degrees it will not
15#     introduce a bezier curve.
16#
17#     If the point where the lines meet has other ways that meet it (at a 'T'
18#     junction for example), it leaves those segments untouched (i.e. it
19#     doesn't introduce curves for those segments).
20#
21#  This is what it does for each street name:
22
23#     Guess the length of the rendered text, and compare it to the
24#     length of the way it's on. If it fits, output it as is. If not,
25#     abbreviate it and try again. There are a range of different
26#     abbreviations including getting rid of the text entirely.
27
28#  Call it as follows:
29#
30#    osm-svg-beautifier.pl YourSVGFile.svg >SVGFileWithCurves.svg
31#
32#-----------------------------------------------------------------------------
33#
34#  Copyright 2007 by Barry Crabtree, Robert Hart
35#
36#  This program is free software; you can redistribute it and/or modify
37#  it under the terms of the GNU General Public License as published by
38#  the Free Software Foundation; either version 2 of the License, or
39#  (at your option) any later version.
40#
41#  This program is distributed in the hope that it will be useful,
42#  but WITHOUT ANY WARRANTY; without even the implied warranty of
43#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
44#  GNU General Public License for more details.
45#
46#  You should have received a copy of the GNU General Public License
47#  along with this program; if not, write to the Free Software
48#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA
49#
50#-----------------------------------------------------------------------------
51
52use strict;
53use Carp;
54use Math::Complex;
55use Math::Vec qw(:terse);
56
57my $min_angle = 0.5;
58my $min_scale = 0;
59#
60# transform linear paths to curves
61#
62# Globals...
63my $line_position = 0; # current line position in the svg file
64my $text_class = "";   # last seen text class
65my @svg_lines = ();    # the lines from the svg file
66my %point_is_in;       # lookup for 'what ways is this point in?'
67my %to_transform;      # ways that need transforming
68my %text_to_transform; # text that needs transforming
69my %path_length;       # lengths of all the paths
70
71# first pass, read in the svg lines, build the %point_is_in @svg_lines &
72# %to_transform structures.
73while (<>) {
74    my $line = $_;
75    $svg_lines[$line_position] = $line;
76    if ( $line =~ m{(<path \s id=\"(?:way|area)_[\w-]+\" \s d=\") # the prefix of the path
77                    ([^\"Z]+)                          # the core path itself
78                    (.*/>)$                           # the rest
79                   }x ) { # found a path
80
81        my $path_prefix    = $1;
82        my $path_statement = $2;
83        my $path_suffix    = $3;
84        my $path_points_ref = path_points($path_statement);
85        my $path_id = $1 if ( $path_prefix =~ /id=\"([^\"]*)\"/ );
86
87        #calculate the length for text fitting purposes.
88        $path_length{"$path_id"} = path_length($path_points_ref);
89
90        foreach my $point (@$path_points_ref) {
91            my $point_index = $point->[0].$point->[1];
92            my $way_list_ref = $point_is_in{$point_index};
93            push @$way_list_ref, $path_prefix;
94            $point_is_in{$point_index} = $way_list_ref;
95        }
96
97        $to_transform{$path_prefix}
98            = [$line_position, $path_statement, $path_suffix, $path_points_ref];
99       
100    }
101    if ( $line =~ /<text.*>/) {
102        #found a possible text element
103        $text_class = $1 if ( $line =~ /class=\"([^\"]*)\"/ ) ; # keep an eye on what the most recent class is.
104        if ( $line =~ /href=\"\#(way[^\"]*)\".*>([^<]*)</ ) {
105            #looks like the label on a way.
106            my $text_path = $1;
107            my $text = $2;
108            $text_to_transform{$text_path} = [$line_position, $text, $text_class];
109        }
110    }
111    $line_position++;
112}
113
114# Second pass, create the bezier curve versions of all the paths that have
115# been found.
116foreach my $path (keys %to_transform) {
117    # transform path_statment to curves
118    my $path_info_ref  = $to_transform{$path};
119    my $line_index     = $path_info_ref->[0];
120    my $path_statement = $path_info_ref->[1];
121    my $path_suffix    = $path_info_ref->[2];
122
123    my $transformed_path = curvify_path($path_statement, $path);
124    $svg_lines[$line_index] = $path.$transformed_path.$path_suffix."\n";
125}
126
127# Third pass, modify text
128foreach my $text_path (keys %text_to_transform) {
129    # transform text
130    my $text_ref   = $text_to_transform{$text_path};
131    my $line_index = $text_ref->[0];
132    my $text = $text_ref->[1];
133    my $text_class = $text_ref->[2];
134    #my $newtext = int($path_length{$text_path}); #puts path length on roads (useful for debugging)
135    my $newtext = abbreviate($text, $path_length{$text_path}, $text_class);
136
137    print STDERR "$text abbreviated to $newtext\n" if($text ne $newtext);
138
139    $svg_lines[$line_index] =~ s/$text/$newtext/;
140}
141
142# print out the transformed svg file.
143foreach my $line (@svg_lines) {
144    print $line;
145}
146
147
148# Get all the points in a path, removing duplicates along the way...
149sub path_points {
150    my $path_string = shift;
151    my $path_points_ref;
152
153    # there may be multiple moves in a path so get each one
154    my @move_segments = split /M\s*/, $path_string;
155
156    foreach my $move_segment (@move_segments) {
157        next if !$move_segment; # there is no pre-seg if there is only 1 'M'
158
159        # get all the points in the path
160        my $tmp_points_ref = [map [split /(?:\s|,)/, $_], split('L', $move_segment)];
161        # stop those occasional divide by zero errors...
162        push @$path_points_ref, @$tmp_points_ref; 
163    }
164
165    my $clean_points_ref = remove_duplicate_points($path_points_ref);
166    return $clean_points_ref;
167}
168
169#calculate length of path (based on straight lines)
170sub path_length {
171    my $points_ref = shift;
172    my $len = 0;
173    for (my $i=0; $i<scalar(@$points_ref)-1; $i++) {
174        $len += sqrt( ($points_ref->[$i]->[0] - $points_ref->[$i-1]->[0])**2 +
175                      ($points_ref->[$i]->[1] - $points_ref->[$i-1]->[1])**2 )
176    }
177    return $len;
178}
179
180sub remove_duplicate_points {
181    my $points_ref = shift;
182
183    my $clean_points_ref = [$points_ref->[0]];
184    shift @$points_ref;
185
186    foreach my $point_ref (@$points_ref) {
187        if ($point_ref->[0].$point_ref->[1] eq
188                $clean_points_ref->[-1][0].$clean_points_ref->[-1][1]) {
189                next;
190        }
191
192        push @$clean_points_ref, $point_ref;
193    }
194    if (scalar(@$points_ref)+1 != scalar(@$clean_points_ref)) {
195    } 
196    return $clean_points_ref;
197}
198
199sub remove_spur_points {
200    my $points_ref = shift;
201
202# spur points are when you get a way that goes A->B->A->C. The point 'B' is a
203# spur point & we don't like 'em.
204
205    my $clean_points_ref = [$points_ref->[0]];
206
207    shift @$points_ref;
208
209    for(my $i=0; $i < scalar(@$points_ref)-1; $i++) {
210        if ($clean_points_ref->[-1][0].$clean_points_ref->[-1][1] ne 
211            $points_ref->[$i+1][0].$points_ref->[$i+1][1]
212           ) {
213           push @$clean_points_ref, $points_ref->[$i];
214        }
215    }
216    push @$clean_points_ref, $points_ref->[-1];
217
218    return $clean_points_ref;
219}
220
221sub dup_points {
222    my $points_ref = shift;
223
224    foreach my $p (@$points_ref) {
225        my $count = 0;
226        foreach my $q (@$points_ref) {
227            if (join('', @$p) eq join('', @$q)) {
228                $count++;
229            }
230        }
231        if ($count > 1) {
232            return 1;
233        }
234    }
235    return;
236}
237
238# splits up the path string and calls 'from_lines_to_curves' appropriately to
239# build up a modified path string.
240sub curvify_path {
241    my $path_string = shift;
242    my $way_id      = shift;
243
244    my $tmp_string = $path_string;
245    $tmp_string =~ s/[^L]//g;
246
247    #
248    if (length($tmp_string) < 1) { # cant do much with a single line segment
249        return $path_string;
250    }
251
252    my $bezier_path_string = q{};
253
254    # there may be multiple moves in a path so get each one
255    my @move_segments = split /M\s*/, $path_string;
256
257    foreach my $move_segment (@move_segments) {
258        my $postfix = q{};
259        next if !$move_segment; # there is no pre-seg if there is only 1 'M'
260
261        if ($move_segment =~ s/Z$//) {
262            $postfix = 'Z';
263        }
264        # get all the points in the path
265        my @path_points = map [split /(?:\s|,)/, $_], split('L', $move_segment);
266
267        if ($way_id =~ /way_/ && dup_points(\@path_points)) {
268            $bezier_path_string .= "M$path_string$postfix"; 
269        } else {
270            $bezier_path_string 
271                .= 'M'.from_lines_to_curves(\@path_points, $way_id).$postfix;
272        }
273    }
274
275    return $bezier_path_string;
276}
277
278# When two ways meet at their ends, we need the second point in the 'other'
279# way to get good control points in the bezier curve. This just returns the
280# second point.
281sub get_second_point {
282    my ($start_point, $way_id) = @_;
283
284# uncomment the line below if you don't want the last segment in a way to be
285# curved when it meets another way.
286#
287#    return undef;
288    my $ways_ref = $point_is_in{$start_point};
289
290    if ($way_id =~ /area_/) { # areas are easier...
291        my $way_points = $to_transform{$way_id}->[3];
292        if ($way_points->[0][0].$way_points->[0][1] eq $start_point) {
293            return $way_points->[-1];
294        } else {
295            return $way_points->[0];
296        }
297    }
298
299    # now do normal ways...
300    # more than two ways meet - dont curve these.
301    return undef if @$ways_ref != 2;
302
303    my $otherway = ($ways_ref->[0] eq $way_id && $ways_ref->[1] ne $way_id)? $ways_ref->[1]: $ways_ref->[0];
304
305    # maybe there wasn't another way.
306    return undef if !$otherway;
307    # maybe this way has a loop in it.
308#    return undef if $otherway eq $way_id;
309
310    my $way_points = $to_transform{$otherway}->[3];
311   
312    if ($start_point eq $way_points->[0][0].$way_points->[0][1]) {
313        return $way_points->[1];
314    } elsif ($start_point eq $way_points->[-1][0].$way_points->[-1][1]) {
315        return $way_points->[-2];
316    }
317
318    return undef;
319}
320
321# heavy work here. Takes the set of points in a path and generates a bezier
322# version where appropriate.
323sub from_lines_to_curves {
324    my $points_ref = shift;
325    my $way_id     = shift;
326
327    my $cp_range = 0.5;
328    my $incremental_string = q{};
329
330    # Add a point at either end of the set of points to make it easy to
331    # generate the correct control points. If this way is standalone, then the
332    # ends of the way are extended straight back from the start/end
333    # points. If it joins another way, then use the second point in the
334    # other way as the end point. This will make ways that join connect
335    # smoothly - a point that Jochn Topf noticed.
336    #
337    my $start_point = $points_ref->[0][0].$points_ref->[0][1];
338    my $end_point   = $points_ref->[-1][0].$points_ref->[-1][1];
339
340    my $second_point_ref;
341   
342    if ($start_point eq $end_point && $way_id =~ /area_/) {
343        $second_point_ref = $points_ref->[-2];
344    } else {
345        $second_point_ref = get_second_point($start_point, $way_id);
346    }
347    if ($second_point_ref && $second_point_ref->[0].$second_point_ref->[1] ne
348    $points_ref->[1][0].$points_ref->[1][1]) {
349        unshift @$points_ref, $second_point_ref;
350    } else { # make a dummy point
351        unshift @$points_ref, [ $points_ref->[0][0]
352                               -$points_ref->[1][0] + $points_ref->[0][0],
353                                $points_ref->[0][1]
354                               -$points_ref->[1][1] + $points_ref->[0][1] ];
355    }
356
357    if ($start_point eq $end_point && $way_id =~ /area_/) {
358        $second_point_ref = $points_ref->[1];
359    } else {
360        $second_point_ref = get_second_point($end_point, $way_id);
361    }
362    if ($second_point_ref && $second_point_ref->[0].$second_point_ref->[1] ne
363    $points_ref->[-2][0].$points_ref->[-2][1]) {
364        push @$points_ref, $second_point_ref;
365    } else { # make a dummy point
366        push @$points_ref, [ $points_ref->[-1][0]
367                            +$points_ref->[-1][0] - $points_ref->[-2][0],
368                             $points_ref->[-1][1]
369                            +$points_ref->[-1][1] - $points_ref->[-2][1] ];
370    }
371
372    $points_ref = remove_duplicate_points(remove_spur_points($points_ref));
373    my $points_in_line     = scalar(@$points_ref);
374    my $current_point      = 0;
375    my $path_start_ref     = shift @$points_ref;
376    my $path_mid_ref       = $points_ref->[0];
377    my $path_end_ref       = $points_ref->[1];
378
379    my ($start_v,  $mid_v,  $end_v, $mid_start_v, $mid_end_v );
380    my ($start_mid_nv, $mid_start_nv, $mid_end_nv);
381    my $control_v;
382    my $control_scale;
383
384    my $pl;
385
386    foreach my $p (@$points_ref) {
387        $pl .= "\n\t".join(':', @$p);
388    }
389#    print "$way_id: $pl\n";
390
391    # go round each set of 3 points to generate the bezier points
392    while (@$points_ref >= 3) {
393        if (!$incremental_string) {
394            $incremental_string = q{ }.$path_mid_ref->[0].q{ }.$path_mid_ref->[1];
395        }
396
397        # decide to use real control points or not. We only use real control
398        # points if this node is only referenced in one 'way' or we are at the
399        # beginning/end of a way that only joins with one other way. This makes ways
400        # that 'T' sharp on the join.
401        if ( $path_start_ref->[0].$path_start_ref->[1] ne
402             $path_end_ref->[0].$path_end_ref->[1] &&
403             $path_mid_ref->[0].$path_mid_ref->[1] ne
404             $points_ref->[2][0].$points_ref->[2][1]
405             && ($way_id =~ /area_/
406            || @{$point_is_in{$path_mid_ref->[0].$path_mid_ref->[1]}} == 1
407            || (   $current_point == 0 
408                && @{$point_is_in{$path_mid_ref->[0].$path_mid_ref->[1]}} == 2)
409            || (   $current_point == $points_in_line-3 
410                && @{$point_is_in{$path_mid_ref->[0].$path_mid_ref->[1]}} == 2)
411            )) {
412
413#           print "\n\t->".join(':', @$path_start_ref)." ".join(':',
414#            @$path_mid_ref)." ".join(':',@$path_end_ref)."\n";
415            $incremental_string .= ' C ';
416            # work out control point 1 from $path_start, $path_mid & $path_end
417            $start_v = V($path_start_ref->[0], $path_start_ref->[1]);
418            $mid_v   = V($path_mid_ref->[0], $path_mid_ref->[1]);
419            $end_v   = V($path_end_ref->[0], $path_end_ref->[1]);
420            $start_mid_nv = U( ($mid_v-$start_v) + ($end_v-$mid_v) );
421            $mid_start_v = V($start_v->[0]-$mid_v->[0], $start_v->[1]-$mid_v->[1]);
422            $mid_end_v   = V($end_v->[0]-$mid_v->[0], $end_v->[1]-$mid_v->[1]);
423            $control_scale = normalise_cp($mid_start_v, $mid_end_v);
424            $control_v = $mid_v + V($start_mid_nv->ScalarMult($control_scale*abs($end_v-$mid_v)*$cp_range));
425
426            $incremental_string .= $control_v->[0].','.$control_v->[1].q{ };
427           
428            # move on a segment
429            $path_start_ref = $path_mid_ref;
430            $path_mid_ref   = $path_end_ref;
431            $path_end_ref   = $points_ref->[2];
432            shift @$points_ref;
433   
434#           print "\n\t-->".join(':', @$path_start_ref)." ".join(':',
435#            @$path_mid_ref)." ".join(':',@$path_end_ref)."\n";
436            # work out control point 2 from new $path_start, $path_mid & path_end
437            $start_v = V($path_start_ref->[0], $path_start_ref->[1]);
438            $mid_v   = V($path_mid_ref->[0], $path_mid_ref->[1]);
439            $end_v   = V($path_end_ref->[0], $path_end_ref->[1]);
440            $start_mid_nv = U( ($start_v-$mid_v) + ($mid_v-$end_v) );
441            $mid_start_v = V($start_v->[0]-$mid_v->[0], $start_v->[1]-$mid_v->[1]);
442            $mid_end_v   = V($end_v->[0]-$mid_v->[0], $end_v->[1]-$mid_v->[1]);
443                $control_scale = normalise_cp($mid_start_v, $mid_end_v);
444            $control_v = $mid_v + V($start_mid_nv->ScalarMult($control_scale * abs($mid_v-$start_v)*$cp_range));
445   
446            $incremental_string .= $control_v->[0].','.$control_v->[1].q{ };
447   
448            $incremental_string .= $path_mid_ref->[0].q{ }.$path_mid_ref->[1].q{ };
449        } else { # make a straight line segment
450            $incremental_string .= 'L'.$path_end_ref->[0].','.$path_end_ref->[1].q{};
451            # move on a segment
452            $path_start_ref = $path_mid_ref;
453            $path_mid_ref   = $path_end_ref;
454            $path_end_ref   = $points_ref->[2];
455            shift @$points_ref; 
456        }
457    }
458   
459    return $incremental_string;
460}
461
462
463# if the angle of the control point is less than 90 degrees return 0
464# between 90 & 180 degrees return a number between 0 & 1.
465sub normalise_cp {
466    my ($start_v, $end_v) = @_;
467    my $PI = 3.1415926;
468    my $max_angle = $PI/2; # 180degrees
469    my $angle = $start_v->InnerAngle($end_v);
470
471    $angle = Re($angle);
472   
473    if ($angle < $PI*$min_angle) { # too small, so
474        return 0;
475    }
476
477    # angle is between $PI/4 and $PI/2
478    $angle = $angle - $PI*$min_angle;
479    return $angle / ($PI*$min_angle);
480}
481
482sub abbreviate {
483    my ($orig_text, $space, $class) = @_;
484
485    #need mechanism to read fontsizes from osmarender style files
486    my %fontsizes = ( "highway-unclassified-name", 1,
487                     "highway-primary-name", 1,
488                     "highway-secondary-name", 1,
489                     "highway-tertiary-name", 1,
490                     "highway-trunk-name", 1.5,
491                     "highway-service-name", 0.3,
492                     "highway-motorway-name", 1.5
493                     );
494
495    #very anglo-centric list. probably should be conf-file.
496    my %abbrevs = ( "North", "N.",
497                    "South", "S.",
498                    "West", "W.",
499                    "East", "E.",
500                    "Saint", "St.",
501                    "Street", "St.",
502                    "Road", "Rd.",
503                    "Avenue", "Ave.",
504                    "Buildings", "Blds.",
505                    "Place", "Pl.",
506                    "Square", "Sq.",
507                    "Great", "Gt.",
508                    "Boulevard", "Bv.",
509                    "Route", "Rt.",
510                    "Passage", "Psg.",
511                    "Motorway", "Mway.",
512                    "Gardens", "Gdns."
513                    );
514
515    return $orig_text if(!defined($space) || $space eq ""); #can't do anything if we don't know required length.
516    return $orig_text if($orig_text=~/&/);                  #cowardly refuse to deal with escaped strings.
517    return $orig_text if(!defined($fontsizes{$class}));     #bailout if we don't know the font size.
518
519    my $maxchars = $space * $fontsizes{$class};             #hack - needs calibrating
520   
521    return $orig_text if (length($orig_text) < $maxchars);  #if text already fits return it
522
523    #work through abbreviations list until text fits.
524
525    foreach my $key (keys(%abbrevs)){
526        $orig_text =~ s/$key/$abbrevs{$key}/ ;
527        return $orig_text if (length($orig_text) < $maxchars);
528    }
529
530    #give up
531    return "";
532
533}
Note: See TracBrowser for help on using the repository browser.