File Coverage

blib/lib/Geo/SpatialDB/Import/OpenStreetMap.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Geo::SpatialDB::Import::OpenStreetMap;
2             $Geo::SpatialDB::Import::OpenStreetMap::VERSION = '0.000_001'; # TRIAL
3              
4 2     2   1074 $Geo::SpatialDB::Import::OpenStreetMap::VERSION = '0.000001';use Moo 2;
  2         34  
  2         11  
5 2     2   440 use Carp;
  2         2  
  2         101  
6 2     2   344 use XML::Parser;
  0            
  0            
7             use Geo::SpatialDB::Location;
8             use Geo::SpatialDB::Path;
9             use Geo::SpatialDB::RouteSegment;
10             use Geo::SpatialDB::Route::Road;
11             use Geo::SpatialDB::Area;
12             use Log::Any '$log';
13             use namespace::clean;
14              
15             # ABSTRACT: Import OpenStreetMap data as SpatialDB Entities
16              
17              
18             has tmp_storage => is => 'lazy';
19             has stats => is => 'lazy';
20             has latlon_precision => is => 'rw', default => sub { 1_000_000 };
21             has entity_id_prefix => is => 'rw';
22              
23             sub _build_stats {
24             my $self= shift;
25             $self->tmp_storage->get('stats') // {};
26             }
27              
28             sub _build_tmp_storage {
29             require File::Temp;
30             require Geo::SpatialDB::Storage::LMDB_Storable;
31             return Geo::SpatialDB::Storage::LMDB_Storable->new(
32             path => File::Temp->newdir('osm-import-XXXXX'),
33             run_with_scissors => 1,
34             );
35             }
36              
37             sub DESTROY {
38             my $self= shift;
39             # When cleaning up, make the storage go out of scope before its path does,
40             # for the case when path is a Tmpdir object which wants to delete everything.
41             my $path= $self->tmp_storage->path
42             if $self->tmp_storage->can('path');
43             delete $self->{tmp_storage};
44             }
45              
46             sub load_xml {
47             my ($self, $source)= @_;
48             my @stack;
49             my $prec= $self->latlon_precision;
50             my $stats= $self->stats;
51             my $stor= $self->tmp_storage;
52             XML::Parser->new( Handlers => {
53             Start => sub {
54             my ($expat, $el, %attr) = @_;
55             push @stack, \%attr;
56             },
57             End => sub {
58             my ($expat, $el) = @_;
59             my $obj = pop @stack;
60             if ($el eq 'tag') {
61             $stack[-1]{tag}{$obj->{k}} = $obj->{v}
62             if @stack;
63             }
64             elsif ($el eq 'nd') {
65             push @{ $stack[-1]{nd} }, $obj->{ref}
66             if @stack;
67             }
68             elsif ($el eq 'member') {
69             push @{ $stack[-1]{member} }, $obj
70             if @stack;
71             }
72             elsif ($el eq 'node') {
73             # Convert lat/lon to microdegree integers
74             my $lat= int( $obj->{lat} * $prec );
75             my $lon= int( $obj->{lon} * $prec );
76             $stats->{node}++;
77             $stats->{node_tag}{$_}++ for keys %{ $obj->{tag} // {} };
78             $stor->put('n'.$obj->{id}, [ $lat, $lon, [], $obj->{tag} ]);
79             }
80             elsif ($el eq 'way') {
81             $stats->{way}++;
82             $stats->{way_tag}{$_}++ for keys %{ $obj->{tag} // {} };
83             $stor->put('w'.$obj->{id}, $obj);
84             }
85             elsif ($el eq 'relation') {
86             $stats->{relation}++;
87             $stats->{relation_tag}{$_}++ for keys %{ $obj->{tag} // {} };
88             $stats->{relation_member_type}{$_}++ for map { $_->{type} // '' } @{ $obj->{member} // []};
89             $stats->{relation_member_role}{$_}++ for map { $_->{role} // '' } @{ $obj->{member} // []};
90             $stor->put('r'.$obj->{id}, $obj);
91             }
92             }
93             })->parse($self->_open_stream($source));
94             $stor->put(stats => $stats);
95             $stor->put(preprocessed => 0);
96             $stor->commit;
97             $self;
98             }
99              
100             sub preprocess {
101             my $self= shift;
102             my $stor= $self->tmp_storage;
103             my $stats= $self->stats;
104             return if $stor->get('preprocessed');
105            
106             my ($way_id, $rel_id, $way, $rel);
107             my ($progress_i, $progress_n, $progress_prev, $progress_ival)= (0, $stats->{way}+$stats->{relation}, -1, 0.05);
108            
109             # Relate nodes to ways that reference them
110             my $i= $stor->iterator('w');
111             while ((($way_id,$way)= $i->()) and $way_id =~ /^w/) {
112             if ($progress_i++ / $progress_n >= $progress_prev + $progress_ival) {
113             $log->info("progress: $progress_i/$progress_n");
114             $progress_prev= ($progress_i-1) / $progress_n;
115             }
116             $stats->{preproc_way}++;
117             for my $node_id (@{ $way->{nd} // [] }) {
118             my $n= $stor->get("n$node_id");
119             if ($n) {
120             push @{ $n->[2] }, $way_id;
121             $stor->put("n$node_id", $n);
122             $stats->{preproc_rewrite_node}++;
123             } else {
124             $log->notice("Way $way_id references missing node $node_id");
125             }
126             }
127             }
128             # Relate nodes and ways to relations that reference them
129             $i= $stor->iterator('r');
130             while ((($rel_id,$rel)= $i->()) and $rel_id =~ /^r/) {
131             if ($progress_i++ / $progress_n >= $progress_prev + $progress_ival) {
132             $log->info("progress: $progress_i/$progress_n");
133             $progress_prev= ($progress_i-1) / $progress_n;
134             }
135             $stats->{preproc_relation}++;
136             for my $m (@{ $rel->{member} // [] }) {
137             my $typ= $m->{type} // '';
138             # If relation mentions a way or node, load it and add the reference
139             # and store it back.
140             if ($typ eq 'node' && $m->{ref}) {
141             my $n= $stor->get("n$m->{ref}");
142             if ($n) {
143             push @{ $n->[2] }, $rel_id;
144             $stor->put("n$m->{ref}");
145             $stats->{preproc_rewrite_node}++;
146             } else {
147             $log->notice("Relation $rel_id references missing node $m->{ref}");
148             }
149             }
150             elsif ($typ eq 'way' && $m->{ref}) {
151             my $way= $stor->get("w$m->{ref}");
152             if ($way) {
153             push @{ $way->{rel} }, $rel_id;
154             $stor->put("w$m->{ref}", $way);
155             $stats->{preproc_rewrite_way}++;
156             }
157             else {
158             $log->notice("Relation $rel_id references missing way $m->{ref}");
159             }
160             }
161             }
162             }
163             $stor->put(stats => $stats);
164             $stor->put(preprocessed => 1);
165             $stor->commit;
166             }
167              
168             sub generate_roads {
169             my ($self, $sdb, %opts)= @_;
170             $self->preprocess;
171             my $stor= $self->tmp_storage;
172             my $stats= $self->stats;
173             my $prefix= $self->entity_id_prefix // '';
174             my $tmp_prefix= "~";
175             my ($progress_i, $progress_n, $progress_prev, $progress_ival)= (0, $stats->{way}, -1, 0.05);
176            
177             my $wanted_subtypes= $opts{type} // {
178             map { $_ => 1 } qw(
179             motorway motorway_link rest_area
180             trunk trunk_link
181             primary primary_link
182             secondary secondary_link
183             tertiary tertiary_link
184             service residential living_street
185             road unclassified track )
186             };
187            
188             # Iterate every 'way' looking for ones with a 'highway' tag
189             my $i= $stor->iterator('w');
190             my ($way_id, $way);
191             while ((($way_id, $way)= $i->()) and $way_id =~ /^w/) {
192             if ($progress_i++ / $progress_n >= $progress_prev + $progress_ival) {
193             $log->info("progress: $progress_i/$progress_n");
194             $progress_prev= ($progress_i-1) / $progress_n;
195             }
196            
197             next unless $way->{tag}{highway} and $wanted_subtypes->{$way->{tag}{highway}};
198            
199             my $type= 'road.' . delete $way->{tag}{highway};
200             $stats->{types}{$type}++;
201            
202             # Delete all "tiger:" tags, for now. (they're not very useful for my purposes)
203             delete $way->{tag}{$_} for grep { /^tiger:/ } keys %{ $way->{tag} };
204              
205             my $seg_idx= 0;
206             my $seg_id= sprintf("%sw%X.%X", $prefix, substr($way_id,1), $seg_idx++);
207             my @path;
208             for my $node_id (@{$way->{nd}}) {
209             my $node= $stor->get("n$node_id");
210             if (!$node) {
211             $log->error("Way $way_id references missing node $node_id");
212             next;
213             }
214             # Is the node referenced by other ways? If so, we create it as a "location".
215             # If not, then we just grab its lat/lon and ignore the rest.
216             # TODO: we should generate an Intersection Location and start a new
217             # RouteSegment each time more than one Way with tag of Highway
218             # shares the same node.
219             my %ref= map { $_ => 1 } @{ $node->[2] };
220             if (1 < keys %ref) {
221             my $loc_id= sprintf("%sn%X", $prefix, $node_id);
222             my $loc= $stor->get("$tmp_prefix$loc_id");
223             if (!$loc) {
224             $loc= Geo::SpatialDB::Location->new(
225             id => $loc_id,
226             type => 'todo',
227             lat => $node->[0],
228             lon => $node->[1],
229             rad => 0,
230             tags => $node->[3],
231             );
232             $stats->{gen_road_loc}++;
233             }
234             $loc->rel([ @{ $loc->rel // [] }, $seg_id ]);
235             $stor->put("$tmp_prefix$loc_id", $loc);
236             push @path, [ $node->[0], $node->[1], $loc_id ];
237             }
238             else {
239             push @path, [ $node->[0], $node->[1] ];
240             }
241             }
242             if (!@path) {
243             $log->notice("Skipping empty path generated from way $way_id");
244             next;
245             }
246             #my $path= Geo::SpatialDB::Path->new(
247             # id => "osm_$way_id",
248             # seq => \@path
249             #);
250             # TODO: There should be multiple of these created
251             my @segments= ( Geo::SpatialDB::RouteSegment->new(
252             id => $seg_id,
253             type => $type,
254             ($way->{tag}{oneway} && $way->{tag}{oneway} eq 'yes'? (oneway => 1) : ()),
255             path => \@path,
256             # TODO: add tags related to road surface or speed limit
257             routes => [],
258             ) );
259              
260             # Each Way becomes a Route. TODO: combine connected routes with the same name
261             # into a single object and concatenate the RouteSegments.
262            
263             # Load or create a "Route" object to represent the name and metadata of this road.
264             my $road; # = TODO: search for road of same name connected to either end of this Way
265             if ($road) {
266             # Merge any tags that make sense to merge
267             } else {
268             # Multiple names are stored in name, name_1, etc
269             my @names= delete $way->{tag}{name};
270             my $i= 1;
271             while (defined $way->{tag}{"name_$i"}) {
272             push @names, delete $way->{tag}{"name_$i"};
273             ++$i;
274             }
275             # TODO: keep only the keys we care about
276             # We don't bother creating a Road entry unless it has a name or tags
277             if (@names || keys %{ $way->{tag} }) {
278             $stats->{gen_road}++;
279             my $route_id= sprintf("%sw%X", $prefix, substr($way_id,1));
280             $road= Geo::SpatialDB::Route::Road->new(
281             id => $route_id,
282             type => $type,
283             names => \@names,
284             tags => $way->{tag},
285             segments => [],
286             );
287             }
288             }
289             if ($road) {
290             # Add segment ref to the route
291             $road->segments([ @{ $road->segments//[] }, map { $_->id } @segments ]);
292             # Add route reference to the segments
293             push @{ $_->routes }, $road->id
294             for @segments;
295             # Store the road temporarily (in case there are more changes needed)
296             $stor->put($tmp_prefix . $road->id, $road);
297             }
298            
299             # Scan the relations mentioning this Way for highway names,
300             # which we create as additional Route entities
301             for my $rel_id (grep { /^r/ } @{ $way->{rel} }) {
302             my $rel= $stor->get($rel_id);
303             if ($rel && ($rel->{tag}{type}//'') eq 'route' && ($rel->{tag}{route}//'') eq 'road') {
304             my $route_id= sprintf("%sr%X", $prefix, substr($rel_id,1));
305             $road= $stor->get("$tmp_prefix$route_id");
306             if (!$road) {
307             $stats->{gen_road}++;
308             my @names= grep { defined } $rel->{tag}{name}, $rel->{tag}{ref};
309             # TODO: keep only the keys we care about
310             if (@names || keys %{ $rel->{tag} }) {
311             $road= Geo::SpatialDB::Route::Road->new(
312             id => $route_id,
313             type => 'road.network',
314             names => \@names,
315             tags => $rel->{tag},
316             segments => [],
317             );
318             }
319             }
320             if ($road) {
321             # Add segment ref to the route
322             $road->segments([ @{ $road->segments//[] }, map { $_->id } @segments ]);
323             # Add route reference to the segments
324             push @{ $_->routes }, $road->id
325             for @segments;
326             # Then store the road again, to tmp storage
327             $stor->put("$tmp_prefix$route_id", $road);
328             }
329             }
330             #if ($rel && ($rel->{tag}{type}//'') eq 'route' && !$rel->{tag}{route}) {
331             # use DDP;
332             # p $rel;
333             #}
334             }
335            
336             # The segments are finished, so we import them
337             $sdb->add_entity($_) for @segments;
338             $stats->{gen_road_seg}+= @segments;
339             $stats->{gen_road_seg_pts}+= scalar @path;
340             }
341            
342             # Now, all segments are imported, but the routes are in tmp storage.
343             # Copy them across.
344             $i= $stor->iterator($tmp_prefix);
345             my ($k, $v);
346             while (($k, $v)= $i->() and index($k, $tmp_prefix)==0) {
347             $sdb->add_entity($v);
348             }
349             $stor->rollback;
350             }
351              
352             sub generate_trails {
353             # TODO:
354             }
355              
356             sub generate_waterways {
357             # TODO: rivers, lakes, etc
358             }
359              
360             sub generate_gov_areas {
361             # TODO: government zones
362             }
363             sub generate_postal_areas {
364             # TODO: postal zones
365             }
366             sub generate_time_zone_areas {
367             # TODO: time zones
368             }
369             sub generate_landuse_areas {
370             # TODO: parks, historic areas, etc
371             }
372              
373             sub _open_stream {
374             my ($self, $thing)= @_;
375             if (!ref($thing) or ref($thing) eq 'SCALAR' or ref($thing) =~ /^Path::Class/) {
376             open my $fh, '<:raw', $thing
377             or die "open('$thing'): $!\n";
378             # Automatic decompression of known file extensions
379             if ($thing =~ /\.bz2$/) {
380             require IO::Uncompress::Bunzip2;
381             $fh= IO::Uncompress::Bunzip2->new($fh);
382             } elsif ($thing =~ /\.gz$/) {
383             require IO::Uncompress::Gunzip;
384             $fh= IO::Uncompress::Gunzip->new($fh);
385             }
386             return $fh;
387             }
388             elsif (ref($thing) eq 'GLOB' or ref($thing) =~ /^IO::/ or $thing->can('read')) {
389             return $thing;
390             }
391             else {
392             croak "Don't know how to read or open $thing";
393             }
394             }
395              
396             1;
397              
398             __END__