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