source: subversion/applications/utils/export/osm2ai/osm2ai.pl @ 17345

Last change on this file since 17345 was 8083, checked in by richard, 12 years ago

version that reads .osm files too

  • Property svn:executable set to *
File size: 16.4 KB
Line 
1#!/usr/bin/perl -w
2
3        # osm2ai.pl
4        # Create Adobe Illustrator 6 file from OpenStreetMap data
5       
6        # Richard Fairhurst/editions Systeme D 2007-8
7        # distributed under the terms of the WTFPL
8        # http://sam.zoy.org/wtfpl/COPYING
9       
10        # Options:
11        # --output                                                      output filename
12        # --filters                                                     filter filename
13
14        # and either:
15        # --input                                                       input filename
16        # or:
17        # --xmin, --xmax, --ymin, --ymax        bounding box
18        # --projection                                          mercator or osgb
19        # --db, --user, --password                      database connection (will use environment variables if present)
20
21        # ----------------------------------------------------------------
22        # To do (also see 'limitations'):
23        # - option to generate grid (OSGB) or use fixed scale
24        # - instead of bounding box, --place=Oxford, --radius=20
25        # - optional join on the way_tags table to reduce number of ways returned
26
27        # ================================================================
28        # Initialise
29       
30        use DBI;
31        use Math::Trig;
32        use Getopt::Long;
33        use Pod::Usage;
34        use Geo::Coordinates::OSGB qw(ll_to_grid);
35
36        # ================================================================
37        # User-defined variables       
38
39        $dbname='openstreetmap'; if (exists $ENV{'DBNAME'}) { $dbname=$ENV{'DBNAME'}; };
40        $dbuser='openstreetmap'; if (exists $ENV{'DBUSER'}) { $dbname=$ENV{'DBUSER'}; };
41        $dbpass='openstreetmap'; if (exists $ENV{'DBPASS'}) { $dbname=$ENV{'DBPASS'}; };
42
43        $xmin=-3.7; $xmax= 1.9; $ymin=50.5 ; $ymax=54.3;        #       $xmin=-2.3; $xmax=-2.2; $ymin=52.15; $ymax=52.25;
44        $proj='mercator';                                                                       #       $proj='osgb';
45        $outfile='output.ai';
46        $infile='';
47        $filters='';
48        $help=$man=0;
49       
50        GetOptions('xmin=f' =>\$xmin, 'xmax=f' =>\$xmax,
51                           'ymin=f' =>\$ymin, 'ymax=f' =>\$ymax,
52                           'projection=s' =>\$proj, 'help|?' => \$help, man => \$man,
53                           'input=s' =>\$infile, 'output=s' =>\$outfile, 'filters=s' =>\$filters,
54                           'password=s' =>\$dbpass, 'user=s' =>\$dbuser, 'db=s' =>\$dbname);
55        pod2usage(1) if $help;
56        pod2usage(-exitstatus => 0, -verbose => 2) if $man;
57        $proj=lc $proj;
58
59        # Read filters
60       
61        if ($filters) {
62                open INFILE,$filters or die "Can't open filters file: $!\n";
63                @conditions=<INFILE>;
64                close INFILE;
65        } else {
66                @conditions=('railway: railway=*',
67                                         'motorway: highway=motorway',
68                                         'trunk: highway=trunk',
69                                         'primary: highway=primary',
70                                         'secondary: highway=secondary',
71                                         'residential: highway=residential',
72                                         'unclassified: highway=unclassified',
73                                         'other highway: highway=*',
74                                         'other: =');
75        }
76
77        # ================================================================
78        # Read ways from file
79
80        if ($infile) {
81                open INFILE,$infile or die "Can't open input file: $!\n";
82                $way=0;
83                $xmin=99999; $xmax=-99999;
84                $ymin=99999; $ymax=-99999;
85                %lat=(); %lon=();
86                %paths=(); # hash of arrays
87                %attributes=(); # hash of hashes
88                print "Reading input file\n";
89                while (<INFILE>) {
90                        chomp ($a=$_);
91                        if ($a=~/<node id="(\d+)" lat="([\d.\-]+)" lon="([\d.\-]+)"/) {
92                                $lat{$1}=$2; $lon{$1}=$3;
93                                if ($2<$ymin) { $ymin=$2; }
94                                if ($2>$ymax) { $ymax=$2; }
95                                if ($3<$xmin) { $xmin=$3; }
96                                if ($3>$xmax) { $xmax=$3; }
97                        } elsif ($a=~/<way id="(\d+)"/) {
98                                $way=$1;
99                                @path=(); %attribute=();
100                                push @waylist,$way;
101                        } elsif ($a=~/<\/way>/) {
102                                $paths{$way}=[@path];
103                                $attributes{$way}={%attribute};
104                                $way=0;
105                                # set path and attributes
106                                # write to waylist
107                        } elsif ($a=~/<nd ref="(\d+)"/) {
108                                push @path,$1;
109                        } elsif ($a=~/<tag k="(.+?)" v="(.+?)"/) {
110                                $attribute{$1}=$2;
111                        }
112                }
113                close INFILE;
114       
115        # ================================================================
116        # Read ways from database
117
118        } else {
119                $dbh=DBI->connect("DBI:mysql:$dbname",$dbuser,$dbpass, { RaiseError =>1 } );
120                print "Getting list of ways\n";
121                @waylist=Which_Ways();
122        }
123
124        # ================================================================
125        # Generate file
126
127        # ----- Set scale
128
129        if ($proj eq 'osgb') {
130                ($basex,$basey)=ll_to_grid($ymin,$xmin); $masterscale=1/250;
131        } else {
132                $baselong=$xmin; $basey=lat2y($ymin);
133                $masterscale=500/($xmax-$xmin);
134        }
135
136        # ----- Process conditions
137
138        %layer=(); @layers=();
139        foreach $condition (@conditions) {
140                die unless $condition=~/^(.+):/;
141                unless (exists $layer{$1}) { unshift @layers,$1; }
142                $layer{$1}=[];
143        }
144
145        # ----- Read all ways
146
147        $i=0; $al=@waylist;
148        foreach $way (@waylist) {
149                $i++;
150                print "Reading way $i of $al\r";
151                if ($infile) {
152                        $newpath=[];                                                                            # Project
153                        foreach $id (@{$paths{$way}}) {
154                                if ($proj eq 'mercator') { $xs=long2coord($lon{$id}); $ys=lat2coord($lat{$id}); }
155                                elsif ($proj eq 'osgb')  { ($xs,$ys)=ll2osgb($lon{$id},$lat{$id}); }
156                                push @{$newpath},[$xs,$ys,$id];
157                        }
158                        $paths{$way}=$newpath;
159                        $attribute=$attributes{$way};
160                } else {
161                        ($path,$attribute)=Get_Way($way);                                       # Read the way
162                        $paths{$way}=$path;
163                        $attributes{$way}=$attribute;
164                }
165
166                CONDS: foreach $condition (@conditions) {                               # Which conditions does it satisfy?
167                        next unless $condition=~/^(.+):\s*(.*)=(.*)$/;          #  |
168                        $l=$1; $k=$2; $v=$3;                                                            #  |
169                        if (exists(${$attribute}{$k})) {                                        #  |
170                                if ($v eq '*' or ${$attribute}{$k} eq $v) {             #  |
171                                        push @{$layer{$l}},$way; last CONDS;            #  |
172                                }                                                                                               #  |
173                        }                                                                                                       #  |
174                        if ($k eq '') {                                                                         #  |
175                                push @{$layer{$l}},$way; last CONDS;                    #  |
176                        }                                                                                                       #  |
177                }
178        }
179
180        # ----- Write file
181
182        open (OUTFILE, ">$outfile") or die "Can't open output file: $!";
183
184        print "Writing file                               \n";
185        Illustrator_Header();
186        foreach $layername (@layers) {
187                Illustrator_New_Layer($layername,"0 0 0 1");
188                foreach $way (@{$layer{$layername}}) {
189                        $path=$paths{$way};
190                        New_Path($attributes{$way});
191                        foreach $row (@{$path}) {
192                                Output_Point($row->[0],$row->[1]);
193                        }
194                }
195                New_Path();
196        }
197
198        Illustrator_Footer();
199        close OUTFILE;
200        unless ($infile) { $dbh->disconnect(); }
201
202
203
204        # ================================================================
205        # OSM database routines
206
207        # ----- Which_Ways
208        #               returns array of ways
209
210        sub Which_Ways {
211                my $tilesql=sql_for_area($ymin,$xmin,$ymax,$xmax,'');
212                $symin=$ymin*10000000; $symax=$ymax*10000000;
213                $sxmin=$xmin*10000000; $sxmax=$xmax*10000000;
214                my $sql=<<EOF;
215SELECT DISTINCT current_way_nodes.id AS wayid
216  FROM current_way_nodes,current_nodes,current_ways
217 WHERE current_nodes.id=current_way_nodes.node_id
218   AND current_nodes.visible=1
219   AND current_ways.id=current_way_nodes.id
220   AND current_ways.visible=1
221   AND ($tilesql)
222   AND (latitude  BETWEEN $symin AND $symax)
223   AND (longitude BETWEEN $sxmin AND $sxmax)
224 ORDER BY wayid
225EOF
226                my $query=$dbh->prepare($sql);
227                my @ways=();
228                $query->execute();
229                while ($wayid=$query->fetchrow_array()) { push @ways,$wayid; }
230                $query->finish();
231                return @ways;
232        }
233       
234        # ----- Get_Way(id)
235        #               returns path array, attributes hash
236       
237        sub Get_Way {
238                my $wayid=$_[0];
239                my ($lat1,$long1,$id1,$lat2,$long2,$id2,$k,$v);
240                my $sql=<<EOF;
241SELECT latitude*0.0000001,longitude*0.0000001,current_nodes.id
242  FROM current_way_nodes,current_nodes
243 WHERE current_way_nodes.id=?
244   AND current_way_nodes.node_id=current_nodes.id
245   AND current_nodes.visible=1
246 ORDER BY sequence_id
247EOF
248                my $path=[];
249                my $query=$dbh->prepare($sql);
250                $query->execute($wayid);
251               
252                while (($lat,$long,$id)=$query->fetchrow_array()) {
253                        if ($proj eq 'mercator') { $xs=long2coord($long); $ys=lat2coord($lat); }
254                        elsif ($proj eq 'osgb')  { ($xs,$ys)=ll2osgb($long,$lat); }
255                        push @{$path},[$xs,$ys,$id];
256                }
257                $query->finish();
258               
259                $query=$dbh->prepare("SELECT k,v FROM current_way_tags WHERE id=?");
260                $query->execute($wayid);
261                my $attributes={};
262                while (($k,$v)=$query->fetchrow_array()) { ${$attributes}{$k}=$v; }
263                $query->finish();
264
265                return ($path,$attributes);
266        }
267
268        # ----- Lat/long <-> coord conversion
269       
270        sub lat2coord   { return  (lat2y($_[0])-$basey)*$masterscale; }
271        sub long2coord  { return      ($_[0]-$baselong)*$masterscale; }
272        sub lat2y           { return 180/pi * log(Math::Trig::tan(pi/4+$_[0]*(pi/180)/2)); }
273
274        sub ll2osgb             { ($e,$n)=ll_to_grid($_[1],$_[0]);
275                                          $n=($n-$basey)*$masterscale;
276                                          $e=($e-$basex)*$masterscale;
277                                          return ($e,$n); }
278
279        # ================================================================
280        # Illustrator routines
281
282        # ----- Write Illustrator header and footer
283
284        sub Illustrator_Header {
285                $start=1;
286                print OUTFILE <<EOF;
287%!PS-Adobe-3.0
288%%Creator: Adobe Illustrator(r) 6.0
289%%For: (geowiki) (geowiki.com)
290%%Title: (geowiki)
291%%CreationDate: (29/9/02) (12:49 pm)
292%%BoundingBox: -3999 -3893 4595 4685
293%%HiResBoundingBox: -3998.05 -3892.05 4594.05 4684.05
294%%DocumentProcessColors: Cyan Magenta Yellow Black
295%%DocumentNeededResources: procset Adobe_level2_AI5 1.0 0
296%%+ procset Adobe_Illustrator_AI6_vars Adobe_Illustrator_AI6
297%%+ procset Adobe_Illustrator_AI5 1.0 0
298%AI5_FileFormat 2.0
299%AI3_ColorUsage: Color
300%%AI6_ColorSeparationSet: 1 1 (AI6 Default Color Separation Set)
301%%+ Options: 1 16 0 1 0 1 1 1 0 1 1 1 1 18 0 0 0 0 0 0 0 0 -1 -1
302%%+ PPD: 1 21 0 0 60 45 2 2 1 0 0 1 0 0 0 0 0 0 0 0 0 0 ()
303%AI3_TemplateBox: 306 396 306 396
304%AI3_TileBox: 30 31 582 761
305%AI3_DocumentPreview: None
306%AI5_ArtSize: 612 792
307%AI5_RulerUnits: 2
308%AI5_ArtFlags: 1 0 0 1 0 0 1 1 0
309%AI5_TargetResolution: 800
310%AI5_NumLayers: 3
311%AI5_OpenToView: -6702 3180 -16 826 581 58 0 1 2 40
312%AI5_OpenViewLayers: 777
313%%EndComments
314%%BeginProlog
315%%IncludeResource: procset Adobe_level2_AI5 1.0 0
316%%IncludeResource: procset Adobe_Illustrator_AI6_vars Adobe_Illustrator_AI6
317%%IncludeResource: procset Adobe_Illustrator_AI5 1.0 0
318%%EndProlog
319%%BeginSetup
320Adobe_level2_AI5 /initialize get exec
321Adobe_ColorImage_AI6 /initialize get exec
322Adobe_Illustrator_AI5 /initialize get exec
323%%EndSetup
324%AI5_BeginLayer
3251 1 1 1 0 0 0 79 128 255 Lb
326(Layer 1) Ln
3270 A
3280 R
3290 G
330800 Ar
3311 J 0 j 1 w 4 M []0 d
332%AI3_Note:
3330 D
3340 XR
335EOF
336        }
337
338        sub Illustrator_New_Layer {
339                my $layername=$_[0];
340                my $colour=$_[1];
341                print OUTFILE <<EOF;
342LB
343%AI5_EndLayer--
344%AI5_BeginLayer
3451 1 1 1 0 0 1 255 79 79 Lb
346($layername) Ln
3470 A
3480 R
349$colour K
350800 Ar
3511 J 0 j 1 w 4 M []0 d
352%AI3_Note:
3530 D
3540 XR
355EOF
356        }
357
358        sub Illustrator_Footer {
359                print OUTFILE <<EOF;
360LB
361%AI5_EndLayer--
362%%PageTrailer
363gsave annotatepage grestore showpage
364%%Trailer
365Adobe_Illustrator_AI5 /terminate get exec
366Adobe_ColorImage_AI6 /terminate get exec
367Adobe_level2_AI5 /terminate get exec
368%%EOF
369EOF
370        }
371
372        sub New_Path {
373                $point='m';
374                if ($start != 1) { print OUTFILE "S\n"; }
375                $start=1;
376                if ($_[0]) {
377                        $keystr="";
378                        foreach $k (keys %{$_[0]}) {
379                                unless ($k eq 'created_by' or $k=~/^osmarender/) { $keystr.="$k=".${$_[0]}{$k}."; "; }
380                        }
381                        $keystr=~s/; $//;
382                        $keystr=substr($keystr,0,240);
383                        print OUTFILE "\%AI3_Note:$keystr\n";
384                }
385        }
386
387
388        sub Output_Point {
389                print OUTFILE "$_[0] $_[1] $point\n";
390                $point='l';
391                $start=0;
392        }
393       
394
395       
396        # ================================================================
397        # OSM quadtile routines
398        # based on original Ruby code by Tom Hughes
399
400        sub tile_for_point {
401                my $lat=$_[0]; my $lon=$_[1];
402                return tile_for_xy(round(($lon+180)*65535/360),round(($lat+90)*65535/180));
403        }
404       
405        sub round {
406                return int($_[0] + .5 * ($_[0] <=> 0));
407        }
408       
409        sub tiles_for_area {
410                my $minlat=$_[0]; my $minlon=$_[1];
411                my $maxlat=$_[2]; my $maxlon=$_[3];
412       
413                $minx=round(($minlon + 180) * 65535 / 360);
414                $maxx=round(($maxlon + 180) * 65535 / 360);
415                $miny=round(($minlat + 90 ) * 65535 / 180);
416                $maxy=round(($maxlat + 90 ) * 65535 / 180);
417                @tiles=();
418       
419                for ($x=$minx; $x<=$maxx; $x++) {
420                        for ($y=$miny; $y<=$maxy; $y++) {
421                                push(@tiles,tile_for_xy($x,$y));
422                        }
423                }
424                return @tiles;
425        }
426       
427        sub tile_for_xy {
428                my $x=$_[0];
429                my $y=$_[1];
430                my $t=0;
431                my $i;
432               
433                for ($i=0; $i<16; $i++) {
434                        $t=$t<<1;
435                        unless (($x & 0x8000)==0) { $t=$t | 1; }
436                        $x<<=1;
437       
438                        $t=$t<< 1;
439                        unless (($y & 0x8000)==0) { $t=$t | 1; }
440                        $y<<=1;
441                }
442                return $t;
443        }
444       
445        sub sql_for_area {
446                my $minlat=$_[0]; my $minlon=$_[1];
447                my $maxlat=$_[2]; my $maxlon=$_[3];
448                my $prefix=$_[4];
449                my @tiles=tiles_for_area($minlat,$minlon,$maxlat,$maxlon);
450       
451                my @singles=();
452                my $sql='';
453                my $tile;
454                my $last=-2;
455                my @run=();
456                my $rl;
457               
458                foreach $tile (sort @tiles) {
459                        if ($tile==$last+1) {
460                                # part of a run, so keep going
461                                push (@run,$tile); 
462                        } else {
463                                # end of a run
464                                $rl=@run;
465                                if ($rl<3) { push (@singles,@run); }
466                                          else { $sql.="${prefix}tile BETWEEN ".$run[0].' AND '.$run[$rl-1]." OR "; }
467                                @run=();
468                                push (@run,$tile); 
469                        }
470                        $last=$tile;
471                }
472                $rl=@run;
473                if ($rl<3) { push (@singles,@run); }
474                          else { $sql.="${prefix}tile BETWEEN ".$run[0].' AND '.$run[$rl-1]." OR "; }
475                if ($#singles>-1) { $sql.="${prefix}tile IN (".join(',',@singles).') '; }
476                $sql=~s/ OR $//;
477                return $sql;
478        }
479
480__END__
481
482=head1 NAME
483
484B<osm2ai.pl>
485
486=head1 DESCRIPTION
487
488osm2ai takes OpenStreetMap data and converts it to a file
489readable by Adobe Illustrator.
490
491You can either take the data from a .osm XML file (via the
492site's Export tab), or for bigger exports, by specifying a
493bounding box to your own OpenStreetMap-like MySQL
494database.
495
496The data is wholly unstyled - the idea is that you make the
497cartographic decisions yourself. Data is grouped into layers
498to help you.
499
500=head1 SYNOPSIS
501
502osm2ai.pl --input map.osm --projection osgb --output mymap.ai
503
504osm2ai.pl --xmin -2.3 --xmax -2.2 --ymin 52.15 --ymax 52.25
505          --projection osgb --output mymap.ai
506
507=head1 OPTIONS
508
509=over 2
510
511=item B<--projection> name
512
513The projection for your map. Should be either B<osgb>
514(Ordnance Survey National Grid) or B<mercator> (spherical
515Mercator).
516
517=item B<--input> filename
518
519Specifies the input OSM XML file, if you're reading from
520file.
521
522=item B<--filters> filename
523
524Specifies a file containing a list of 'filters'. These are
525used to put appropriately tagged ways in the right layers.
526
527=item B<--output> filename
528
529Specifies the output filename. Defaults to output.ai.
530
531=item B<--xmin> longitude
532B<--xmax> longitude
533B<--ymin> latitude
534B<--ymax> latitude
535
536The bounding box of the area you want to extract, if you're
537reading from a database.
538
539=item B<--db> database_name
540B<--user> database_user
541B<--password> database_password
542
543Connection details for the MySQL database which contains the
544data, if you're reading from a database. If you don't supply
545this, the DBNAME, DBUSER and DBPASS environment variables
546will be used. If they're not set, it'll default to
547openstreetmap, openstreetmap and openstreetmap.
548
549=item B<--man>
550
551Output the full documentation.
552
553=head1 FILTERS
554
555Rather than just bunching everything into one layer, this
556script can filter by tag. So you could put primary roads in
557one layer, secondary in another, and ignore canals
558completely.
559
560Create a plain text file, and add lines like this:
561
562B<motorway: highway=motorway>
563
564Means "put ways tagged with highway=motorway in a 'motorway'
565layer".
566
567B<railway: railway=*>
568
569Means "put ways with any railway tag whatsoever in a 'railway'
570layer.
571
572B<other: =>
573
574Means "put anything else in an 'other' layer".
575
576The tests are carried out in the order you give them. A way
577will only ever be put into one layer, even if it fulfils
578two conditions.
579
580=head1 SETTING UP A DATABASE
581
582If you want to make a map of a greater area than is available
583through the site's Export tab, you will need to set up a MySQL
584database and populate it with OpenStreetMap data. This will
585typically involve downloading B<planet.osm> and then uploading
586it using a program such as B<planetosm-to-db.pl>.
587
588For details, see http://wiki.openstreetmap.org/index.php/Planet.osm
589
590=head1 OUTPUT
591
592The resulting file is Illustrator v6 format (sometimes known as
593'legacy'), which can be opened in any version of Illustrator
594from then on.
595
596For each way, the tags are saved in the 'Attributes' field. You
597can see this by clicking the way in Illustrator, then showing the
598Attributes window (Window->Attributes).
599
600The ways aren't cropped to the bounding box.
601
602=head1 PREREQUISITES
603
604This script needs four modules which you almost certainly have
605(DBI, Math::Trig, Pod::Usage, Getopt::Long) and one which you
606might not (Geo::Coordinates::OSGB).
607
608=head1 LIMITATIONS
609
610It doesn't do POIs or relations, only tagged ways.
611
612The quadtile stuff really ought to be in a library.
613
614The whole caboodle should be on the OSM Export tab.
615
616There should be a grid, or constant scale, or something, so you can mix and
617match different maps.
618
619Reading from an .osm file has been kludged on really messily.
620
621=head1 COPYRIGHT
622
623Written by Richard Fairhurst, 2007-2008.
624
625Quadtile code adapted from Tom Hughes' Ruby OSM server code -
626thanks Tom!
627
628This program really is free software. It's distributed under
629the terms of the WTFPL. You may do what the fuck you want to
630with it. See http://sam.zoy.org/wtfpl/COPYING for details.
631
632If you use it to extract data from OpenStreetMap which isn't
633yours, the output must of course only be published under
634the terms of whatever licence applies to the data.
635
636=cut
Note: See TracBrowser for help on using the repository browser.