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