File Coverage

blib/lib/Map/Tube.pm
Criterion Covered Total %
statement 3 5 60.0
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 5 7 71.4


line stmt bran cond sub pod time code
1             package Map::Tube;
2              
3             $Map::Tube::VERSION = '3.40';
4             $Map::Tube::AUTHORITY = 'cpan:MANWAR';
5              
6             =head1 NAME
7              
8             Map::Tube - Lightweight Routing Framework.
9              
10             =head1 VERSION
11              
12             Version 3.40
13              
14             =cut
15              
16 1     1   47161 use 5.006;
  1         3  
17 1     1   1467 use XML::Twig;
  0            
  0            
18             use Data::Dumper;
19             use Map::Tube::Node;
20             use Map::Tube::Line;
21             use Map::Tube::Table;
22             use Map::Tube::Route;
23             use Map::Tube::Pluggable;
24             use Map::Tube::Exception::MissingMapData;
25             use Map::Tube::Exception::MissingStationName;
26             use Map::Tube::Exception::InvalidStationName;
27             use Map::Tube::Exception::MissingStationId;
28             use Map::Tube::Exception::InvalidStationId;
29             use Map::Tube::Exception::MissingLineId;
30             use Map::Tube::Exception::InvalidLineId;
31             use Map::Tube::Exception::MissingLineName;
32             use Map::Tube::Exception::InvalidLineName;
33             use Map::Tube::Exception::InvalidLineColor;
34             use Map::Tube::Exception::FoundMultiLinedStation;
35             use Map::Tube::Exception::FoundMultiLinkedStation;
36             use Map::Tube::Exception::FoundSelfLinkedStation;
37             use Map::Tube::Exception::DuplicateStationId;
38             use Map::Tube::Exception::DuplicateStationName;
39             use Map::Tube::Exception::MissingPluginGraph;
40             use Map::Tube::Exception::MissingPluginFormatter;
41             use Map::Tube::Exception::MissingPluginFuzzyFind;
42             use Map::Tube::Exception::MalformedMapData;
43             use Map::Tube::Utils qw(to_perl is_same trim common_lines get_method_map is_valid_color);
44             use Map::Tube::Types qw(Routes Tables Lines NodeMap LineMap);
45              
46             use Moo::Role;
47             use Role::Tiny qw();
48             use namespace::autoclean;
49              
50             =encoding utf8
51              
52             =head1 DESCRIPTION
53              
54             The core module defined as Role (Moo) to process the map data. It provides the
55             the interface to find the shortest route in terms of stoppage between two nodes.
56             Also you can get all possible routes between two given nodes.
57              
58             If you are keen to know the internals of L then please follow the note
59             documented in L.
60              
61             =head1 MAP LEADER BOARD
62              
63             +---------------------+--------+--------------------------------------------+
64             | Author | PAUSE | Map Count (City) |
65             +---------------------+--------+--------------------------------------------+
66             | Michal Josef Spacek | SKIM | 22 (Bucharest, Budapest, Dnipropetrovsk, |
67             | | | Kazan, Kharkiv, Kiev, KualaLumpur, Malaga, |
68             | | | Minsk, Moscow, Nanjing, NizhnyNovgorod, |
69             | | | Novosibirsk, Prague, SaintPetersburg, |
70             | | | Samara, Singapore, Sofia, Tbilisi, Vienna, |
71             | | | Warsaw, Yekaterinburg) |
72             | | | |
73             | Mohammad S Anwar | MANWAR | 6 (Barcelona, Delhi, Kolkatta, London, NYC,|
74             | | | Tokyo) |
75             | | | |
76             | Gisbert W Selker | GWS | 4 (Beijing, Glasgow, KoeinBonn, Lyon) |
77             | | | |
78             | Slaven Rezic | SREZIC | 1 (Berlin) |
79             +---------------------+--------+--------------------------------------------+
80              
81             =cut
82              
83             has [qw(name name_to_id plugins _active_link _other_links _line_stations _common_lines)] => (is => 'rw');
84             has experimental => (is => 'ro', default => sub { 0 });
85             has nodes => (is => 'rw', isa => NodeMap);
86             has lines => (is => 'rw', isa => Lines );
87             has tables => (is => 'rw', isa => Tables );
88             has routes => (is => 'rw', isa => Routes );
89             has _lines => (is => 'rw', isa => LineMap);
90              
91             our $AUTOLOAD;
92              
93             sub AUTOLOAD {
94              
95             my $name = $AUTOLOAD;
96             $name =~ s/.*://;
97              
98             my @caller = caller(0);
99             @caller = caller(2) if $caller[3] eq '(eval)';
100              
101             my $method_map = get_method_map();
102             if (exists $method_map->{$name}) {
103             my $module = $method_map->{$name}->{module};
104             my $exception = $method_map->{$name}->{exception};
105             $exception->throw({
106             method => "${module}::${name}",
107             message => "ERROR: Missing plugin $module.",
108             filename => $caller[1],
109             line_number => $caller[2] });
110             }
111             }
112              
113             sub BUILD {
114             my ($self) = @_;
115              
116             # Handle lazy attributes.
117             my @attributes = (keys %{Moo->_constructor_maker_for(ref($self))->all_attribute_specs});
118             unless ((grep /^xml$/, @attributes) || (grep /^json$/, @attributes)) {
119             die "ERROR: Can't apply Map::Tube role, missing 'xml' or 'json'.";
120             }
121              
122             $self->_init_map;
123             $self->_load_plugins;
124             }
125              
126             =head1 SYNOPSIS
127              
128             =head2 Common Usage
129              
130             use strict; use warnings;
131             use Map::Tube::London;
132              
133             my $tube = Map::Tube::London->new;
134             print $tube->get_shortest_route('Baker Street', 'Euston Square'), "\n";
135              
136             You should expect the result like below:
137              
138             Baker Street (Circle, Hammersmith & City, Bakerloo, Metropolitan, Jubilee), Great Portland Street (Circle, Hammersmith & City, Metropolitan), Euston Square (Circle, Hammersmith & City, Metropolitan)
139              
140             =head2 Special Usage
141              
142             use strict; use warnings;
143             use Map::Tube::London;
144              
145             my $tube = Map::Tube::London->new;
146             print $tube->get_shortest_route('Baker Street', 'Euston Square')->preferred, "\n";
147              
148             You should now expect the result like below:
149              
150             Baker Street (Circle, Hammersmith & City, Metropolitan), Great Portland Street (Circle, Hammersmith & City, Metropolitan), Euston Square (Circle, Hammersmith & City, Metropolitan)
151              
152             =head1 METHODS
153              
154             =head2 get_shortest_routes($from, $to)
155              
156             It expects C<$from> and C<$to> station name, required param. It returns an object
157             of type L. On error it throws exception of type L.
158              
159             =cut
160              
161             sub get_shortest_route {
162             my ($self, $from, $to) = @_;
163              
164             ($from, $to) =
165             $self->_validate_input('get_shortest_route', $from, $to);
166              
167             my $_from = $self->get_node_by_id($from);
168             my $_to = $self->get_node_by_id($to);
169              
170             $self->_capture_common_lines($_from, $_to);
171              
172             $self->_get_shortest_route($from);
173              
174             my $nodes = [];
175             while (defined($to) && !(is_same($from, $to))) {
176             push @$nodes, $self->get_node_by_id($to);
177             $to = $self->_get_path($to);
178             }
179              
180             push @$nodes, $_from;
181              
182             return Map::Tube::Route->new(
183             { from => $_from,
184             to => $_to,
185             nodes => [ reverse(@$nodes) ] } );
186             }
187              
188             =head2 get_all_routes($from, $to) *** EXPERIMENTAL ***
189              
190             It expects C<$from> and C<$to> station name, required param. It returns ref to a
191             list of objects of type L. On error it throws exception of type
192             L.
193              
194             Be carefull when using against a large map. You may encounter warning similar to
195             as shown below when run against London map.
196              
197             Deep recursion on subroutine "Map::Tube::_get_all_routes"
198              
199             However for comparatively smaller map, like below,it is happy to give all routes.
200              
201             A(1) ---- B(2)
202             / \
203             C(3) -------- F(6) --- G(7) ---- H(8)
204             \ /
205             D(4) ---- E(5)
206              
207             =cut
208              
209             sub get_all_routes {
210             my ($self, $from, $to) = @_;
211              
212             ($from, $to) =
213             $self->_validate_input('get_all_routes', $from, $to);
214              
215             return $self->_get_all_routes([ $from ], $to);
216             }
217              
218             =head2 name()
219              
220             Returns map name.
221              
222             =head2 get_node_by_id($node_id)
223              
224             Returns an object of type L.
225              
226             =cut
227              
228             sub get_node_by_id {
229             my ($self, $id) = @_;
230              
231             my @caller = caller(0);
232             @caller = caller(2) if $caller[3] eq '(eval)';
233             Map::Tube::Exception::MissingStationId->throw({
234             method => __PACKAGE__."::get_node_by_id",
235             message => "ERROR: Missing Station ID.",
236             filename => $caller[1],
237             line_number => $caller[2] }) unless defined $id;
238              
239             my $node = $self->{nodes}->{$id};
240             Map::Tube::Exception::InvalidStationId->throw({
241             method => __PACKAGE__."::get_node_by_id",
242             message => "ERROR: Invalid Station ID [$id].",
243             filename => $caller[1],
244             line_number => $caller[2] }) unless defined $node;
245              
246             # Check if the node name appears more than once with different id.
247             my @nodes = $self->_get_node_id($node->name);
248             return $node if (scalar(@nodes) == 1);
249              
250             my $lines = {};
251             foreach my $l (@{$node->line}) {
252             $lines->{$l->name} = $l if defined $l->name;
253             }
254             foreach my $i (@nodes) {
255             foreach my $j (@{$self->{nodes}->{$i}->line}) {
256             $lines->{$j->name} = $j if defined $j->name;
257             }
258             }
259             $node->line([ values %$lines ]);
260              
261             return $node;
262             }
263              
264             =head2 get_node_by_name($node_name)
265              
266             Returns ref to a list of object(s) of type L matching node name
267             C<$node_name> in scalar context otherwise returns just a list.
268              
269             =cut
270              
271             sub get_node_by_name {
272             my ($self, $name) = @_;
273              
274             my @caller = caller(0);
275             @caller = caller(2) if $caller[3] eq '(eval)';
276             Map::Tube::Exception::MissingStationName->throw({
277             method => __PACKAGE__."::get_node_by_name",
278             message => "ERROR: Missing Station Name.",
279             filename => $caller[1],
280             line_number => $caller[2] }) unless defined $name;
281              
282             my @nodes = $self->_get_node_id($name);
283             Map::Tube::Exception::InvalidStationName->throw({
284             method => __PACKAGE__."::get_node_by_name",
285             message => "ERROR: Invalid Station Name [$name].",
286             filename => $caller[1],
287             line_number => $caller[2] }) unless scalar(@nodes);
288              
289             my $nodes = [];
290             foreach (@nodes) {
291             push @$nodes, $self->get_node_by_id($_);
292             }
293              
294             if (wantarray) {
295             return @{$nodes};
296             }
297             else {
298             return $nodes->[0];
299             }
300             }
301              
302             =head2 get_line_by_id($line_id)
303              
304             Returns an object of type L.
305              
306             =cut
307              
308             sub get_line_by_id {
309             my ($self, $id) = @_;
310              
311             my @caller = caller(0);
312             @caller = caller(2) if $caller[3] eq '(eval)';
313             Map::Tube::Exception::MissingLineId->throw({
314             method => __PACKAGE__."::get_line_by_id",
315             message => "ERROR: Missing Line ID.",
316             filename => $caller[1],
317             line_number => $caller[2] }) unless defined $id;
318              
319             my $line = $self->_get_line_object_by_id($id);
320             Map::Tube::Exception::InvalidLineId->throw({
321             method => __PACKAGE__."::get_line_by_id",
322             message => "ERROR: Invalid Line ID [$id].",
323             filename => $caller[1],
324             line_number => $caller[2] }) unless defined $line;
325              
326             return $line;
327             }
328              
329             =head2 get_line_by_name($line_name)
330              
331             Returns an object of type L.
332              
333             =cut
334              
335             sub get_line_by_name {
336             my ($self, $name) = @_;
337              
338             my @caller = caller(0);
339             @caller = caller(2) if $caller[3] eq '(eval)';
340             Map::Tube::Exception::MissingLineName->throw({
341             method => __PACKAGE__."::get_line_by_name",
342             message => "ERROR: Missing Line Name.",
343             filename => $caller[1],
344             line_number => $caller[2] }) unless defined $name;
345              
346             my $line = $self->_get_line_object_by_name($name);
347             Map::Tube::Exception::InvalidLineName->throw({
348             method => __PACKAGE__."::get_line_by_name",
349             message => "ERROR: Invalid Line Name [$name].",
350             filename => $caller[1],
351             line_number => $caller[2] }) unless defined $line;
352              
353             return $line;
354             }
355              
356             =head2 get_lines()
357              
358             Returns ref to a list of objects of type L.
359              
360             =cut
361              
362             sub get_lines {
363             my ($self) = @_;
364              
365             my $lines = [];
366             my $other_links = $self->_other_links;
367             foreach (@{$self->{lines}}) {
368             next if exists $other_links->{uc($_->id)};
369             push @$lines, $_ if defined $_->name;
370             }
371              
372             return $lines;
373             }
374              
375             =head2 get_stations($line_name)
376              
377             Returns ref to a list of objects of type L for the C<$line_name>.
378             If C<$line_name> is missing, it would return all stations in the map.
379              
380             =cut
381              
382             sub get_stations {
383             my ($self, $line_name) = @_;
384              
385             my $lines = [];
386             my $stations = [];
387             my $seen = {};
388              
389             if (defined $line_name) {
390             my @caller = caller(0);
391             @caller = caller(2) if $caller[3] eq '(eval)';
392              
393             my $line = $self->_get_line_object_by_name($line_name);
394             Map::Tube::Exception::InvalidLineName->throw({
395             method => __PACKAGE__."::get_stations",
396             message => "ERROR: Invalid Line Name [$line_name].",
397             filename => $caller[1],
398             line_number => $caller[2] })
399             unless defined $line;
400              
401             $lines = [ $self->_get_line_object_by_name($line_name) ];
402             }
403             else {
404             $lines = $self->get_lines;
405             }
406              
407             foreach my $line (@$lines) {
408             foreach my $station (@{$line->{stations}}) {
409             unless (exists $seen->{$station->id}) {
410             push @$stations, $self->get_node_by_id($station->id);
411             $seen->{$station->id} = 1;
412             }
413             }
414             }
415              
416             return $stations;
417             }
418              
419             #
420             #
421             # DO NOT MAKE IT PUBLIC
422              
423             sub get_map_data {
424             my ($self, $caller, $method) = @_;
425              
426             my $data;
427             my $xml = $self->xml;
428             if ($xml ne '') {
429             eval {
430             $data = XML::Twig->new->parsefile($xml)->simplify(keyattr => 'stations', forcearray => 0);
431             # Handle if there is only one line.
432             my $lines = $data->{lines}->{line};
433             if (ref($lines) eq 'HASH') {
434             $data->{lines}->{line} = [ $lines ];
435             }
436             };
437             return $data unless ($@);
438              
439             Map::Tube::Exception::MalformedMapData->throw({
440             method => $method,
441             message => "ERROR: Malformed Map Data ($xml).",
442             filename => $caller->[1],
443             line_number => $caller->[2] });
444             }
445             else {
446             my $json = $self->json;
447             if ($json ne '') {
448             eval { $data = to_perl($json) };
449             return $data unless ($@);
450              
451             Map::Tube::Exception::MalformedMapData->throw({
452             method => $method,
453             message => "ERROR: Malformed Map Data ($json).",
454             filename => $caller->[1],
455             line_number => $caller->[2] });
456             }
457             else {
458             if (!defined $caller) {
459             $method = __PACKAGE__.'::get_map_data';
460             my @_caller = caller(0);
461             @_caller = caller(2) if $_caller[3] eq '(eval)';
462             $caller = \@_caller;
463             }
464              
465             Map::Tube::Exception::MissingMapData->throw({
466             method => $method,
467             message => "ERROR: Missing Map Data.",
468             filename => $caller->[1],
469             line_number => $caller->[2] });
470             }
471             }
472             }
473              
474             =head1 PLUGINS
475              
476             =head2 * L
477              
478             The L plugin add the support to generate the entire map
479             or map for a particular line as base64 encoded string (png image).
480              
481             Please refer to the L for more details.
482              
483             =head2 * L
484              
485             The L plugin adds the support to format the object
486             supported by the plugin.
487              
488             Please refer to the L for more info.
489              
490             =head2 * L
491              
492             Gisbert W. Selke, built the add-on for L to find stations and lines by
493             name, possibly partly or inexactly specified. The module is a Moo role which gets
494             plugged into the Map::Tube::* family automatically once it is installed.
495              
496             Please refer to the L for more info.
497              
498             =head1 MAP DATA FORMAT
499              
500             Map data can be represented in JSON or XML format. The preferred format is JSON.
501             C or above comes with a handy script C, that
502             can be used to change the data format of an existing map data.Below is how we can
503             represet the sample map:
504              
505             A(1) ---- B(2)
506             / \
507             C(3) -------- F(6) --- G(7) ---- H(8)
508             \ /
509             D(4) ---- E(5)
510              
511             =head2 JSON
512              
513             {
514             "name" : "sample map",
515             "lines" : {
516             "line" : [
517             { "id" : "A", "name" : "A", "color" : "red" },
518             { "id" : "B", "name" : "B", "color" : "#FFFF00" }
519             ]
520             },
521             "stations" : {
522             "station" : [
523             { "id" : "A1", "name" : "A1", "line" : "A", "link" : "B2,C3" },
524             { "id" : "B2", "name" : "B2", "line" : "A", "link" : "A1,F6" },
525             { "id" : "C3", "name" : "C3", "line" : "A,B", "link" : "A1,D4,F6" },
526             { "id" : "D4", "name" : "D4", "line" : "A,B", "link" : "C3,E5" },
527             { "id" : "E5", "name" : "E5", "line" : "B", "link" : "D4,F6" },
528             { "id" : "F6", "name" : "F6", "line" : "B", "link" : "B2,C3,E5" },
529             { "id" : "G7", "name" : "G7", "line" : "B", "link" : "F6,H8" },
530             { "id" : "H8", "name" : "H8", "line" : "B", "link" : "G7" }
531             ]
532             }
533             }
534              
535             =head2 XML
536              
537            
538            
539            
540            
541            
542            
543            
544            
545            
546            
547            
548            
549            
550            
551            
552            
553            
554              
555             =head1 MAP VALIDATION
556              
557             =head2 DATA VALIDATION
558              
559             The package L can easily be used to validate raw map data.Anyone
560             building a new map using L is advised to have a unit test as a part of
561             their distribution.Just like in L package,there is a unit test
562             something like below:
563              
564             use strict; use warnings;
565             use Test::More;
566             use Map::Tube::London;
567              
568             eval "use Test::Map::Tube";
569             plan skip_all => "Test::Map::Tube required" if $@;
570              
571             ok_map(Map::Tube::London->new);
572              
573             =head2 FUNCTIONAL VALIDATION
574              
575             The package L v0.09 or above can easily be used to validate map
576             basic functions provided by L.
577              
578             use strict; use warnings;
579             use Test::More;
580              
581             my $min_ver = 0.09;
582             eval "use Test::Map::Tube $min_ver";
583             plan skip_all => "Test::Map::Tube $min_ver required" if $@;
584              
585             use Map::Tube::London;
586             ok_map_functions(Map::Tube::London->new);
587              
588             The package L v0.17 or above can easily be used to validate map
589             routing functions provided by L.
590              
591             use strict; use warnings;
592             use Test::More;
593              
594             my $min_ver = 0.17;
595             eval "use Test::Map::Tube $min_ver tests => 1";
596             plan skip_all => "Test::Map::Tube $min_ver required" if $@;
597              
598             use Map::Tube::London;
599             my $map = Map::Tube::London->new;
600              
601             my @routes = (
602             "Route 1|Tower Gateway|Aldgate|Tower Gateway,Tower Hill,Aldgate",
603             "Route 2|Liverpool Street|Monument|Liverpool Street,Bank,Monument",
604             );
605              
606             ok_map_routes($map, \@routes);
607              
608             =cut
609              
610             #
611             #
612             # PRIVATE METHODS
613              
614             sub _get_shortest_route {
615             my ($self, $from) = @_;
616              
617             my $nodes = [];
618             my $index = 0;
619             my $seen = {};
620              
621             $self->_init_table;
622             $self->_set_length($from, $index);
623             $self->_set_path($from, $from);
624              
625             my $all_nodes = $self->{nodes};
626             while (defined($from)) {
627             my $length = $self->_get_length($from);
628             my $f_node = $all_nodes->{$from};
629             $self->_set_active_links($f_node);
630              
631             if (defined $f_node) {
632             my $links = [ split /\,/, $f_node->{link} ];
633             while (scalar(@$links) > 0) {
634             my ($success, $link) = $self->_get_next_link($from, $seen, $links);
635             $success or ($links = [ grep(!/\b$link\b/, @$links) ]) and next;
636              
637             if (($self->_get_length($link) == 0) || ($length > ($index + 1))) {
638             $self->_set_length($link, $length + 1);
639             $self->_set_path($link, $from);
640             push @$nodes, $link;
641             }
642              
643             $seen->{$link} = 1;
644             $links = [ grep(!/\b$link\b/, @$links) ];
645             }
646             }
647              
648             $index = $length + 1;
649             $from = shift @$nodes;
650             $nodes = [ grep(!/\b$from\b/, @$nodes) ] if defined $from;
651             }
652             }
653              
654             sub _get_all_routes {
655             my ($self, $visited, $to) = @_;
656              
657             my $last = $visited->[-1];
658             my $nodes = $self->get_node_by_id($last)->link;
659             foreach my $id (split /\,/, $nodes) {
660             next if _is_visited($id, $visited);
661              
662             if (is_same($id, $to)) {
663             push @$visited, $id;
664             $self->_set_routes($visited);
665             pop @$visited;
666             last;
667             }
668             }
669              
670             foreach my $id (split /\,/, $nodes) {
671             next if (_is_visited($id, $visited) || is_same($id, $to));
672              
673             push @$visited, $id;
674             $self->_get_all_routes($visited, $to);
675             pop @$visited;
676             }
677              
678             return $self->{routes};
679             }
680              
681             sub _map_node_name {
682             my ($self, $name, $id) = @_;
683              
684             push @{$self->{name_to_id}->{uc($name)}}, $id;
685             }
686              
687             sub _validate_input {
688             my ($self, $method, $from, $to) = @_;
689              
690             my @caller = caller(0);
691             @caller = caller(2) if $caller[3] eq '(eval)';
692              
693             Map::Tube::Exception::MissingStationName->throw({
694             method => __PACKAGE__."::$method",
695             message => "ERROR: Missing Station Name.",
696             filename => $caller[1],
697             line_number => $caller[2] })
698             unless (defined($from) && defined($to));
699              
700             $from = trim($from);
701             my $_from = $self->get_node_by_name($from);
702              
703             $to = trim($to);
704             my $_to = $self->get_node_by_name($to);
705              
706             return ($_from->{id}, $_to->{id});
707             }
708              
709             sub _init_map {
710             my ($self) = @_;
711              
712             my $_lines = {};
713             my $lines = {};
714             my $nodes = {};
715             my $tables = {};
716             my $_other_links = {};
717             my $_seen_nodes = {};
718              
719             my @caller = caller(0);
720             @caller = caller(2) if $caller[3] eq '(eval)';
721              
722             my $method = __PACKAGE__."::_init_map";
723             my $data = $self->get_map_data(\@caller, $method);
724             $self->{name} = $data->{name};
725              
726             my $name_to_id = $self->{name_to_id};
727             my $has_station_index = {};
728             foreach my $station (@{$data->{stations}->{station}}) {
729             my $id = $station->{id};
730              
731             Map::Tube::Exception::DuplicateStationId->throw({
732             method => $method,
733             message => "ERROR: Duplicate Station ID [$id].",
734             filename => $caller[1],
735             line_number => $caller[2] }) if (exists $_seen_nodes->{$id});
736              
737             $_seen_nodes->{$id} = 1;
738             my $name = $station->{name};
739              
740             Map::Tube::Exception::DuplicateStationName->throw({
741             method => $method,
742             message => "ERROR: Duplicate Station Name [$name].",
743             filename => $caller[1],
744             line_number => $caller[2] }) if (defined $name_to_id->{uc($name)});
745              
746             $self->_map_node_name($name, $id);
747             $tables->{$id} = Map::Tube::Table->new({ id => $id });
748              
749             my $_station_lines = [];
750             foreach my $_line (split /\,/, $station->{line}) {
751             if ($_line =~ /\:/) {
752             $_line = $self->_capture_line_station($_line, $id);
753             $has_station_index->{$_line} = 1;
754             }
755             my $uc_line = uc($_line);
756             my $line = $lines->{$uc_line};
757             $line = Map::Tube::Line->new({ id => $_line }) unless defined $line;
758             $_lines->{$uc_line} = $line;
759             $lines->{$uc_line} = $line;
760             push @$_station_lines, $line;
761             }
762              
763             if (exists $station->{other_link} && defined $station->{other_link}) {
764             my @link_nodes = ();
765             foreach my $_entry (split /\,/, $station->{other_link}) {
766             my ($_link_type, $_nodes) = split /\:/, $_entry, 2;
767             my $uc_link_type = uc($_link_type);
768             my $line = $lines->{$uc_link_type};
769             $line = Map::Tube::Line->new({ id => $_link_type, name => $_link_type }) unless defined $line;
770             $_lines->{$uc_link_type} = $line;
771             $lines->{$uc_link_type} = $line;
772             $_other_links->{$uc_link_type} = 1;
773              
774             push @$_station_lines, $line;
775             push @link_nodes, (split /\|/, $_nodes);
776             }
777              
778             $station->{link} .= "," . join(",", @link_nodes);
779             }
780              
781             $station->{line} = $_station_lines;
782             my $node = Map::Tube::Node->new($station);
783             $nodes->{$id} = $node;
784              
785             foreach my $line (@{$_station_lines}) {
786             next if exists $has_station_index->{$line->id};
787             push @{$line->{stations}}, $node;
788             }
789             }
790              
791             my @lines;
792             if (exists $data->{lines} && exists $data->{lines}->{line}) {
793             @lines = (ref $data->{lines}->{line} eq 'HASH')
794             ? ($data->{lines}->{line})
795             : @{$data->{lines}->{line}};
796             }
797              
798             foreach my $_line (@lines) {
799             my $uc_line = uc($_line->{id});
800             my $line = $_lines->{$uc_line};
801             if (defined $line) {
802             $line->{name} = $_line->{name};
803             $line->{color} = $_line->{color};
804             if ($has_station_index) {
805             foreach (sort { $a <=> $b } keys %{$self->{_line_stations}->{$uc_line}}) {
806             my $station_id = $self->{_line_stations}->{$uc_line}->{$_};
807             $line->add_station($nodes->{$station_id});
808             }
809             }
810             $_lines->{$uc_line} = $line;
811             }
812             }
813              
814             $self->_order_station_lines($nodes);
815              
816             $self->lines([ values %$lines ]);
817             $self->_lines($_lines);
818             $self->_other_links($_other_links);
819             $self->nodes($nodes);
820             $self->tables($tables);
821             }
822              
823             sub _init_table {
824             my ($self) = @_;
825              
826             foreach my $id (keys %{$self->{tables}}) {
827             $self->{tables}->{$id}->{path} = undef;
828             $self->{tables}->{$id}->{length} = 0;
829             }
830              
831             $self->{_active_links} = undef;
832             }
833              
834             sub _load_plugins {
835             my ($self) = @_;
836              
837             $self->{plugins} = [ Map::Tube::Pluggable::plugins ];
838             foreach (@{$self->plugins}) {
839             Role::Tiny->apply_roles_to_object($self, $_);
840             }
841             }
842              
843             sub _capture_common_lines {
844             my ($self, $from, $to) = @_;
845              
846             my $from_lines = [ map { $_->id } @{$from->line} ];
847             my $to_lines = [ map { $_->id } @{$to->line} ];
848              
849             $self->{_common_lines} = [ common_lines($from_lines, $to_lines) ];
850             }
851              
852             sub _get_next_link {
853             my ($self, $from, $seen, $links) = @_;
854              
855             my $nodes = $self->{nodes};
856             my $active_links = $self->{_active_links};
857             my @common_lines = common_lines($active_links->[0], $active_links->[1]);
858              
859             if ($self->{experimental} && scalar(@{$self->{_common_lines}})) {
860             @common_lines = (@{$self->{_common_lines}}, @common_lines);
861             }
862              
863             my $link = undef;
864             foreach my $_link (@$links) {
865             return (0, $_link) if ((exists $seen->{$_link}) || ($from eq $_link));
866              
867             my $node = $nodes->{$_link};
868             next unless defined $node;
869              
870             my @lines = ();
871             foreach (@{$node->{line}}) { push @lines, $_->{id}; }
872              
873             my @common = common_lines(\@common_lines, \@lines);
874             return (1, $_link) if (scalar(@common) > 0);
875              
876             $link = $_link;
877             }
878              
879             return (1, $link);
880             }
881              
882             sub _set_active_links {
883             my ($self, $node) = @_;
884              
885             my $active_links = $self->{_active_links};
886             my $links = [ split /\,/, $node->{link} ];
887              
888             if (defined $active_links) {
889             shift @$active_links;
890             push @$active_links, $links;
891             }
892             else {
893             push @$active_links, $links;
894             push @$active_links, $links;
895             }
896              
897             $self->{_active_links} = $active_links;
898             }
899              
900             sub _validate_map_data {
901             my ($self) = @_;
902              
903             my @caller = caller(0);
904             @caller = caller(2) if $caller[3] eq '(eval)';
905             my $nodes = $self->{nodes};
906             my $seen = {};
907              
908             $self->_validate_lines(\@caller);
909              
910             foreach my $id (keys %$nodes) {
911              
912             Map::Tube::Exception::InvalidStationId->throw({
913             method => __PACKAGE__."::_validate_map_data",
914             message => "ERROR: Station ID can't have ',' character.",
915             filename => $caller[1],
916             line_number => $caller[2] }) if ($id =~ /\,/);
917              
918             my $node = $nodes->{$id};
919              
920             $self->_validate_nodes(\@caller, $nodes, $node, $seen);
921             $self->_validate_self_linked_nodes(\@caller, $node, $id);
922             $self->_validate_multi_linked_nodes(\@caller, $node, $id);
923             $self->_validate_multi_lined_nodes(\@caller, $node, $id);
924             }
925             }
926              
927             sub _validate_lines {
928             my ($self, $caller) = @_;
929              
930             my $lines = $self->{lines};
931             foreach (@$lines) {
932             my $line_color = $_->{color};
933             if (defined $line_color && !(is_valid_color($line_color))) {
934             Map::Tube::Exception::InvalidLineColor->throw({
935             method => __PACKAGE__."::_validate_map_data",
936             message => "ERROR: Invalid Line Color [$line_color].",
937             filename => $caller->[1],
938             line_number => $caller->[2] });
939             }
940             }
941             }
942              
943             sub _validate_nodes {
944             my ($self, $caller, $nodes, $node, $seen) = @_;
945              
946             foreach (split /\,/, $node->{link}) {
947             next if (exists $seen->{$_});
948             my $_node = $nodes->{$_};
949              
950             Map::Tube::Exception::InvalidStationId->throw({
951             method => __PACKAGE__."::_validate_map_data",
952             message => "ERROR: Invalid Station ID [$_].",
953             filename => $caller->[1],
954             line_number => $caller->[2] }) unless (defined $_node);
955              
956             $seen->{$_} = 1;
957             }
958             }
959              
960             sub _validate_self_linked_nodes {
961             my ($self, $caller, $node, $id) = @_;
962              
963             if (grep { $_ eq $id } (split /\,/, $node->{link})) {
964             Map::Tube::Exception::FoundSelfLinkedStation->throw({
965             method => __PACKAGE__."::_validate_map_data",
966             message => sprintf("ERROR: %s is self linked,", $id),
967             filename => $caller->[1],
968             line_number => $caller->[2] });
969             }
970             }
971              
972             sub _validate_multi_linked_nodes {
973             my ($self, $caller, $node, $id) = @_;
974              
975             my %links = ();
976             my $max_link = 1;
977              
978             foreach my $link (split( /\,/, $node->{link})) {
979             $links{$link}++;
980             }
981              
982             foreach (keys %links) {
983             $max_link = $links{$_} if ($max_link < $links{$_});
984             }
985              
986             if ($max_link > 1) {
987             my $message = sprintf("ERROR: %s linked to %s multiple times,",
988             $id, join( ',', grep { $links{$_} > 1 } keys %links));
989              
990             Map::Tube::Exception::FoundMultiLinkedStation->throw({
991             method => __PACKAGE__."::_validate_map_data",
992             message => $message,
993             filename => $caller->[1],
994             line_number => $caller->[2] });
995             }
996             }
997              
998             sub _capture_line_station {
999             my ($self, $line, $station_id) = @_;
1000              
1001             my ($line_id, $sequence) = split /\:/, $line, 2;
1002             $self->{_line_stations}->{uc($line_id)}->{$sequence} = $station_id;
1003              
1004             return $line_id;
1005             }
1006              
1007             sub _validate_multi_lined_nodes {
1008             my ($self, $caller, $node, $id) = @_;
1009              
1010             my %lines = ();
1011             foreach (@{$node->{line}}) { $lines{$_->{id}}++; }
1012              
1013             my $max_link = 1;
1014             foreach (keys %lines) {
1015             $max_link = $lines{$_} if ($max_link < $lines{$_});
1016             }
1017              
1018             if ($max_link > 1) {
1019             my $message = sprintf("ERROR: %s has multiple lines %s,",
1020             $id, join( ',', grep { $lines{$_} > 1 } keys %lines));
1021              
1022             Map::Tube::Exception::FoundMultiLinedStation->throw({
1023             method => __PACKAGE__."::_validate_map_data",
1024             message => $message,
1025             filename => $caller->[1],
1026             line_number => $caller->[2] });
1027             }
1028             }
1029              
1030             sub _set_routes {
1031             my ($self, $routes) = @_;
1032              
1033             my $_routes = [];
1034             my $nodes = $self->{nodes};
1035             foreach my $id (@$routes) {
1036             push @$_routes, $nodes->{$id};
1037             }
1038              
1039             my $from = $_routes->[0];
1040             my $to = $_routes->[-1];
1041             my $route = Map::Tube::Route->new({ from => $from, to => $to, nodes => $_routes });
1042             push @{$self->{routes}}, $route;
1043             }
1044              
1045             sub _get_path {
1046             my ($self, $id) = @_;
1047              
1048             return $self->{tables}->{$id}->{path};
1049             }
1050              
1051             sub _set_path {
1052             my ($self, $id, $node_id) = @_;
1053              
1054             return unless (defined $id && defined $node_id);
1055             $self->{tables}->{$id}->{path} = $node_id;
1056             }
1057              
1058             sub _get_length {
1059             my ($self, $id) = @_;
1060              
1061             return 0 unless (defined $id && defined $self->{tables}->{$id});
1062             return $self->{tables}->{$id}->{length};
1063             }
1064              
1065             sub _set_length {
1066             my ($self, $id, $value) = @_;
1067              
1068             return unless (defined $id && defined $value);
1069             $self->{tables}->{$id}->{length} = $value;
1070             }
1071              
1072             sub _get_table {
1073             my ($self, $id) = @_;
1074              
1075             return $self->{tables}->{$id};
1076             }
1077              
1078             sub _get_node_id {
1079             my ($self, $name) = @_;
1080              
1081             my $nodes = $self->{name_to_id}->{uc($name)};
1082             return unless defined $nodes;
1083              
1084             if (wantarray) {
1085             return @{$nodes};
1086             }
1087             else {
1088             return $nodes->[0];
1089             }
1090             }
1091              
1092             sub _get_line_object_by_name {
1093             my ($self, $name) = @_;
1094              
1095             $name = uc($name);
1096             foreach my $line_id (keys %{$self->{_lines}}) {
1097             my $line = $self->{_lines}->{$line_id};
1098             if (defined $line && defined $line->name) {
1099             return $line if ($name eq uc($line->name));
1100             }
1101             }
1102              
1103             return;
1104             }
1105              
1106             sub _get_line_object_by_id {
1107             my ($self, $id) = @_;
1108              
1109             $id = uc($id);
1110             foreach my $line_id (keys %{$self->{_lines}}) {
1111              
1112             my $line = $self->{_lines}->{$line_id};
1113             if (defined $line && defined $line->name) {
1114             return $line if ($id eq uc($line->id));
1115             }
1116             }
1117              
1118             return;
1119             }
1120              
1121             sub _order_station_lines {
1122             my ($self, $nodes) = @_;
1123              
1124             return unless scalar(keys %$nodes);
1125              
1126             foreach my $node (keys %$nodes) {
1127             my $_lines_h = {};
1128             foreach (@{$nodes->{$node}->{line}}) {
1129             $_lines_h->{$_->id} = $_ if defined $_->name;
1130             }
1131             my $_lines_a = [];
1132             foreach (sort keys %$_lines_h) {
1133             push @$_lines_a, $_lines_h->{$_};
1134             }
1135             $nodes->{$node}->line($_lines_a);
1136             }
1137             }
1138              
1139             sub _is_visited {
1140             my ($id, $list) = @_;
1141              
1142             foreach (@$list) {
1143             return 1 if is_same($_, $id);
1144             }
1145              
1146             return 0;
1147             }
1148              
1149             =head1 AUTHOR
1150              
1151             Mohammad S Anwar, C<< >>
1152              
1153             =head1 REPOSITORY
1154              
1155             L
1156              
1157             =head1 SEE ALSO
1158              
1159             =over 2
1160              
1161             =item * L
1162              
1163             =item * L
1164              
1165             =item * L
1166              
1167             =back
1168              
1169             =head1 CONTRIBUTORS
1170              
1171             =over 2
1172              
1173             =item * Gisbert W. Selke, C<< >>
1174              
1175             =item * Michal Špaček, C<< >>
1176              
1177             =back
1178              
1179             =head1 BUGS
1180              
1181             Please report any bugs or feature requests to C, or
1182             through the web interface at L.
1183             I will be notified and then you'll automatically be notified of progress on your
1184             bug as I make changes.
1185              
1186             =head1 SUPPORT
1187              
1188             You can find documentation for this module with the perldoc command.
1189              
1190             perldoc Map::Tube
1191              
1192             You can also look for information at:
1193              
1194             =over 4
1195              
1196             =item * RT: CPAN's request tracker (report bugs here)
1197              
1198             L
1199              
1200             =item * AnnoCPAN: Annotated CPAN documentation
1201              
1202             L
1203              
1204             =item * CPAN Ratings
1205              
1206             L
1207              
1208             =item * Search CPAN
1209              
1210             L
1211              
1212             =back
1213              
1214             =head1 LICENSE AND COPYRIGHT
1215              
1216             Copyright (C) 2010 - 2016 Mohammad S Anwar.
1217              
1218             This program is free software; you can redistribute it and / or modify it under
1219             the terms of the the Artistic License (2.0). You may obtain a copy of the full
1220             license at:
1221              
1222             L
1223              
1224             Any use, modification, and distribution of the Standard or Modified Versions is
1225             governed by this Artistic License.By using, modifying or distributing the Package,
1226             you accept this license. Do not use, modify, or distribute the Package, if you do
1227             not accept this license.
1228              
1229             If your Modified Version has been derived from a Modified Version made by someone
1230             other than you,you are nevertheless required to ensure that your Modified Version
1231             complies with the requirements of this license.
1232              
1233             This license does not grant you the right to use any trademark, service mark,
1234             tradename, or logo of the Copyright Holder.
1235              
1236             This license includes the non-exclusive, worldwide, free-of-charge patent license
1237             to make, have made, use, offer to sell, sell, import and otherwise transfer the
1238             Package with respect to any patent claims licensable by the Copyright Holder that
1239             are necessarily infringed by the Package. If you institute patent litigation
1240             (including a cross-claim or counterclaim) against any party alleging that the
1241             Package constitutes direct or contributory patent infringement,then this Artistic
1242             License to you shall terminate on the date that such litigation is filed.
1243              
1244             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND
1245             CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED
1246             WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
1247             NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS
1248             REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT,
1249             INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE
1250             OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1251              
1252             =cut
1253              
1254             1; # End of Map::Tube