1 package Path; 2 # 3 # $RCSfile: Path.pm,v $ 4 # $Date: 2008/04/25 00:01:15 $ 5 # $Revision: 1.10 $ 6 # 7 # Author: Manish Sud <msud@san.rr.com> 8 # 9 # Copyright (C) 2004-2008 Manish Sud. All rights reserved. 10 # 11 # This file is part of MayaChemTools. 12 # 13 # MayaChemTools is free software; you can redistribute it and/or modify it under 14 # the terms of the GNU Lesser General Public License as published by the Free 15 # Software Foundation; either version 3 of the License, or (at your option) any 16 # later version. 17 # 18 # MayaChemTools is distributed in the hope that it will be useful, but without 19 # any warranty; without even the implied warranty of merchantability of fitness 20 # for a particular purpose. See the GNU Lesser General Public License for more 21 # details. 22 # 23 # You should have received a copy of the GNU Lesser General Public License 24 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or 25 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330, 26 # Boston, MA, 02111-1307, USA. 27 # 28 use 5.006; 29 use strict; 30 use Carp; 31 use Exporter; 32 use Storable (); 33 use Scalar::Util (); 34 35 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); 36 37 $VERSION = '1.00'; 38 @ISA = qw(Exporter); 39 @EXPORT = qw(); 40 @EXPORT_OK = qw(); 41 42 %EXPORT_TAGS = (all => [@EXPORT, @EXPORT_OK]); 43 44 # Setup class variables... 45 my($ClassName, $ObjectID); 46 _InitializeClass(); 47 48 # Overload Perl functions... 49 use overload '""' => 'StringifyPath', 50 51 '==' => '_PathEqualOperator', 52 'eq' => '_PathEqualOperator', 53 54 'fallback' => undef; 55 56 # Class constructor... 57 sub new { 58 my($Class, @VertexIDs) = @_; 59 60 # Initialize object... 61 my $This = {}; 62 bless $This, ref($Class) || $Class; 63 $This->_InitializePath(); 64 65 if (@VertexIDs) { $This->AddVertices(@VertexIDs); } 66 67 return $This; 68 } 69 70 # Initialize object data... 71 # 72 sub _InitializePath { 73 my($This) = @_; 74 75 @{$This->{Vertices}} = (); 76 } 77 78 # Initialize class ... 79 sub _InitializeClass { 80 #Class name... 81 $ClassName = __PACKAGE__; 82 } 83 84 # Add a vertex to path after the end vertex... 85 # 86 sub AddVertex { 87 my($This, $VertexID) = @_; 88 89 if (!defined $VertexID ) { 90 carp "Warning: ${ClassName}->AddVertex: No vertex added: Vertex ID must be specified..."; 91 return undef; 92 } 93 push @{$This->{Vertices}}, $VertexID; 94 95 return $This; 96 } 97 98 # Add vertices to the path after the end vertex... 99 # 100 sub AddVertices { 101 my($This, @VertexIDs) = @_; 102 103 if (!@VertexIDs) { 104 carp "Warning: ${ClassName}->AddVertices: No vertices added: Vertices list is empty..."; 105 return undef; 106 } 107 push @{$This->{Vertices}}, @VertexIDs; 108 109 return $This; 110 } 111 112 # Add a vertex to path after the end vertex... 113 # 114 sub PushVertex { 115 my($This, $VertexID) = @_; 116 117 return $This->AddVertex($VertexID); 118 } 119 120 # Add vertices to the path after the end vertex... 121 # 122 sub PushVertices { 123 my($This, @VertexIDs) = @_; 124 125 return $This->AddVertices(@VertexIDs); 126 } 127 128 # Remove end vertex from path... 129 # 130 sub PopVertex { 131 my($This) = @_; 132 133 if (!@{$This->{Vertices}}) { 134 carp "Warning: ${ClassName}->PopVertex: No vertex removed: Path is empty..."; 135 return undef; 136 } 137 pop @{$This->{Vertices}}; 138 139 return $This; 140 } 141 142 # Remove start vertex from path... 143 # 144 sub ShiftVertex { 145 my($This) = @_; 146 147 if (!@{$This->{Vertices}}) { 148 carp "Warning: ${ClassName}->ShiftVertex: No vertex removed: Path is empty..."; 149 return undef; 150 } 151 shift @{$This->{Vertices}}; 152 153 return $This; 154 } 155 156 # Add a vertex to path before the start vertex... 157 # 158 sub UnshiftVertex { 159 my($This, $VertexID) = @_; 160 161 if (!defined $VertexID ) { 162 carp "Warning: ${ClassName}->UnshiftVertex: No vertex added: Vertex ID must be specified..."; 163 return undef; 164 } 165 unshift @{$This->{Vertices}}, $VertexID; 166 167 return $This; 168 } 169 170 # Add vertices to the path before the start vertex... 171 # 172 sub UnshiftVertices { 173 my($This, @VertexIDs) = @_; 174 175 if (!@VertexIDs) { 176 carp "Warning: ${ClassName}->UnshiftVertices: No vertices added: Vertices list is empty..."; 177 return undef; 178 } 179 unshift @{$This->{Vertices}}, @VertexIDs; 180 181 return $This; 182 } 183 184 # Get length... 185 # 186 sub GetLength { 187 my($This) = @_; 188 189 return scalar @{$This->{Vertices}}; 190 } 191 192 # Get start vertex... 193 # 194 sub GetStartVertex { 195 my($This) = @_; 196 197 if (!$This->GetLength()) { 198 return undef; 199 } 200 my($Index) = 0; 201 return $This->_GetVertex($Index); 202 } 203 204 # Get end vertex... 205 # 206 sub GetEndVertex { 207 my($This) = @_; 208 209 if (!$This->GetLength()) { 210 return undef; 211 } 212 my($Index); 213 214 $Index = $This->GetLength() - 1; 215 return $This->_GetVertex($Index); 216 } 217 218 # Get start and end vertices... 219 # 220 sub GetTerminalVertices { 221 my($This) = @_; 222 223 return ( $This->GetStartVertex(), $This->GetEndVertex() ), 224 } 225 226 # Get path vertices... 227 # 228 sub GetVertices { 229 my($This) = @_; 230 231 return wantarray ? @{$This->{Vertices}} : scalar @{$This->{Vertices}}; 232 } 233 234 # Get a specific vertex from path with indicies starting from 0... 235 # 236 sub GetVertex { 237 my($This, $Index) = @_; 238 239 if ($Index < 0) { 240 croak "Error: ${ClassName}->GetValue: Index value must be a positive number..."; 241 } 242 if ($Index >= $This->GetLength()) { 243 croak "Error: ${ClassName}->GetValue: Index vaue must be less than length of path..."; 244 } 245 if (!$This->GetLength()) { 246 return undef; 247 } 248 return $This->_GetVertex($Index); 249 } 250 251 # Get a vertex... 252 # 253 sub _GetVertex { 254 my($This, $Index) = @_; 255 256 return $This->{Vertices}[$Index]; 257 } 258 259 # Get path edges as pair of vertices or number of edges... 260 # 261 sub GetEdges { 262 my($This) = @_; 263 264 if ($This->GetLength < 1) { 265 return undef; 266 } 267 # Set up edges... 268 my($Index, $VertexID1, $VertexID2, @Vertices, @Edges); 269 270 @Edges = (); 271 for $Index (0 .. ($#{$This->{Vertices}} - 1) ) { 272 $VertexID1 = $This->{Vertices}[$Index]; 273 $VertexID2 = $This->{Vertices}[$Index + 1]; 274 push @Edges, ($VertexID1, $VertexID2); 275 } 276 277 return wantarray ? @Edges : ((scalar @Edges)/2); 278 } 279 280 # Is it a cycle? 281 # 282 sub IsCycle { 283 my($This) = @_; 284 my($StartVertex, $EndVertex); 285 286 ($StartVertex, $EndVertex) = $This->GetTerminalVertices(); 287 288 return ($StartVertex == $EndVertex) ? 1 : 0; 289 } 290 291 # For a path to be an independent path, it must meet the following conditions: 292 # . All other vertices are unique. 293 # 294 sub IsIndependentPath { 295 my($This) = @_; 296 297 # Make sure it has at least two vertices... 298 if ($This->GetLength() < 2) { 299 return 0; 300 } 301 302 # Check frequency of occurence for non-terminal vertices... 303 my($VertexID, $IndependenceStatus, @Vertices, %VerticesMap); 304 305 @Vertices = $This->GetVertices(); 306 shift @Vertices; pop @Vertices; 307 308 %VerticesMap = (); 309 $IndependenceStatus = 1; 310 311 VERTEXID: for $VertexID (@Vertices) { 312 if (exists $VerticesMap{$VertexID} ) { 313 $IndependenceStatus = 0; 314 last VERTEXID; 315 } 316 $VerticesMap{$VertexID} = $VertexID; 317 } 318 return $IndependenceStatus; 319 } 320 321 # For a path to be an independent cyclic path, it must meet the following conditions: 322 # . Termimal vertices are the same 323 # . All other vertices are unique. 324 # 325 sub IsIndependentCyclicPath { 326 my($This) = @_; 327 328 # Make sure it's a cycle... 329 if (!($This->GetLength() >= 3 && $This->IsCycle())) { 330 return 0; 331 } 332 return $This->IsIndependentPath(); 333 } 334 335 # Is it a path object? 336 sub IsPath ($) { 337 my($Object) = @_; 338 339 return _IsPath($Object); 340 } 341 342 # Copy path... 343 # 344 sub Copy { 345 my($This) = @_; 346 my($NewPath); 347 348 $NewPath = Storable::dclone($This); 349 350 return $NewPath; 351 } 352 353 # Reverse order of vertices in path... 354 # 355 sub Reverse { 356 my($This) = @_; 357 my(@VertexIDs); 358 359 @VertexIDs = (); push @VertexIDs, @{$This->{Vertices}}; 360 361 @{$This->{Vertices}} = (); push @{$This->{Vertices}}, reverse @VertexIDs; 362 363 return $This; 364 } 365 366 # Get vertices common between two paths... 367 # 368 sub GetCommonVertices { 369 my($This, $Other) = @_; 370 my($VertexID, @CommonVertices, %OtherVerticesMap); 371 372 # Setup a vertices hash for a quick look up... 373 %OtherVerticesMap = (); 374 for $VertexID ($Other->GetVertices()) { 375 $OtherVerticesMap{$VertexID} = $VertexID; 376 } 377 378 @CommonVertices = (); 379 for $VertexID ($This->GetVertices()) { 380 if ($OtherVerticesMap{$VertexID}) { 381 push @CommonVertices, $VertexID 382 } 383 } 384 return wantarray ? @CommonVertices : scalar @CommonVertices; 385 } 386 387 # Join the existing path with a new path specifed using a path object of a list of 388 # verticies. 389 # 390 sub Join { 391 my($This, @Values) = @_; 392 393 return $This->_Join(@Values); 394 } 395 396 # Join the existing path with a new path specifed using a path object at a specified 397 # vertex. 398 # 399 sub JoinAtVertex { 400 my($This, $Other, $CenterVertexID) = @_; 401 402 # Make sure CenterVertexID is end vertex in This and start vertex in Other before 403 # joining them... 404 if ($This->GetEndVertex() != $CenterVertexID) { 405 $This->Reverse(); 406 } 407 if ($Other->GetStartVertex() != $CenterVertexID) { 408 $Other->Reverse(); 409 } 410 return $This->_Join($Other); 411 } 412 413 # Join the existing path with a new path specifed using a path object of a list of 414 # verticies. 415 # 416 # Notes: 417 # . Paths must have a common terminal vertex. 418 # . Based on the common terminal vertex found, new path vertices are added to the 419 # current path in one of the four ways: 420 # . New path at end of current path with same vertices order : EndVertex = NewStartVertex 421 # . New path at end of current path with reversed vertices order: EndVertex = NewEndVertex 422 # . New path at front of current path with same vertices order: StartVertex = NewEndVertex 423 # . New path at front of current path with reversed vertices order: StartVertex = NewStartVertex 424 # 425 sub _Join { 426 my($This, @Values) = @_; 427 428 if (!@Values) { 429 return; 430 } 431 432 # Get a list of new vertex IDs.. 433 my($NewPath, $FirstValue, $TypeOfFirstValue, @NewVertexIDs); 434 435 $NewPath = $This->Copy(); 436 437 @NewVertexIDs = (); 438 $FirstValue = $Values[0]; 439 $TypeOfFirstValue = ref $FirstValue; 440 if ($TypeOfFirstValue =~ /^(SCALAR|HASH|CODE|REF|GLOB)/) { 441 croak "Error: ${ClassName}->JoinPath: Trying to add vertices to path object with a reference to unsupported value format..."; 442 } 443 444 if (_IsPath($FirstValue)) { 445 # It's another path object... 446 push @NewVertexIDs, @{$FirstValue->{Vertices}}; 447 } 448 elsif ($TypeOfFirstValue =~ /^ARRAY/) { 449 # It's array reference... 450 push @NewVertexIDs, @{$FirstValue}; 451 } 452 else { 453 # It's a list of values... 454 push @NewVertexIDs, @Values; 455 } 456 my($StartVertex, $EndVertex, $NewStartVertex, $NewEndVertex); 457 458 ($StartVertex, $EndVertex) = $NewPath->GetTerminalVertices(); 459 ($NewStartVertex, $NewEndVertex) = ($NewVertexIDs[0], $NewVertexIDs[$#NewVertexIDs]); 460 461 if (!($EndVertex == $NewStartVertex || $EndVertex == $NewEndVertex || $StartVertex == $NewEndVertex || $StartVertex == $NewStartVertex)) { 462 carp "Warning: ${ClassName}->JoinPath: Paths can't be joined: No common terminal vertex found..."; 463 return undef; 464 } 465 466 if ($EndVertex == $NewStartVertex) { 467 # Take out EndVertex and add new path at the end... 468 pop @{$NewPath->{Vertices}}; 469 push @{$NewPath->{Vertices}}, @NewVertexIDs; 470 } 471 elsif ($EndVertex == $NewEndVertex) { 472 # Take out EndVertex and add new path at the end with reversed vertex order... 473 pop @{$NewPath->{Vertices}}; 474 push @{$NewPath->{Vertices}}, reverse @NewVertexIDs; 475 } 476 elsif ($StartVertex == $NewEndVertex) { 477 # Take out NewEndVertex and add new path at the front... 478 pop @NewVertexIDs; 479 unshift @{$NewPath->{Vertices}}, @NewVertexIDs; 480 } 481 elsif ($StartVertex == $NewStartVertex) { 482 # Take out NewStartVertex and add new path at the front... 483 shift @NewVertexIDs; 484 unshift @{$NewPath->{Vertices}}, reverse @NewVertexIDs; 485 } 486 487 return $NewPath, 488 } 489 490 # Compare two paths... 491 # 492 sub _PathEqualOperator { 493 my($This, $Other) = @_; 494 495 if (!(defined($This) && _IsPath($This) && defined($Other) && _IsPath($Other))) { 496 croak "Error: ${ClassName}->_PathEqualOperator: Path equal comparison failed: Both object must be paths..."; 497 } 498 499 if ($This->GetLength() != $Other->GetLength()) { 500 return 0; 501 } 502 my($ThisID, $OtherID, $ReverseOtherID); 503 504 $ThisID = join('-', @{$This->{Vertices}}); 505 $OtherID = join('-', @{$Other->{Vertices}}); 506 $ReverseOtherID = join('-', reverse(@{$Other->{Vertices}})); 507 508 return ($ThisID =~ /^($OtherID|$ReverseOtherID)$/) ? 1 : 0; 509 } 510 511 # Return a string containing vertices in the path... 512 sub StringifyPath { 513 my($This) = @_; 514 my($PathString); 515 516 $PathString = "Path: " . join('-', @{$This->{Vertices}}); 517 518 return $PathString; 519 } 520 521 # Is it a path object? 522 sub _IsPath { 523 my($Object) = @_; 524 525 return (Scalar::Util::blessed($Object) && $Object->isa($ClassName)) ? 1 : 0; 526 } 527