package Graph::Dijkstra;

use strict;
use warnings;

use Carp qw(croak carp);

use English qw(-no_match_vars);
$OUTPUT_AUTOFLUSH=1;

 
use vars qw($VERSION);
$VERSION = '0.41';

my $VERBOSE = 0;

use Readonly;

Readonly my $EMPTY_STRING => q{};
Readonly my %IS_GRAPHML_WEIGHT_ATTR => map { ($_ => 1) } qw(weight value cost distance height);
Readonly my %IS_GRAPHML_LABEL_ATTR => map { ($_ => 1) } qw(label name description nlabel);
Readonly my $PINF => 1e9999;         # positive infinity
Readonly my @GRAPH_ATTRIBUTES => qw(label creator);
Readonly my @NODE_ATTRIBUTES => qw(label);
	
## no critic (PostfixControls)

#############################################################################
#used Modules                                                               #
#############################################################################


use Benchmark qw(:hireswallclock);
use Array::Heap::ModifiablePriorityQueue;
use Scalar::Util qw(looks_like_number);


#############################################################################
#Class Methods                                                              #
#############################################################################

sub VERBOSE {
	my $either = shift;
	$VERBOSE = shift;
	print "verbose output set\n" if ($VERBOSE);
	return $either;
}


sub _initialize {
	my ($self, $options) = @_;
	$self->{graph} = ();
	foreach my $attrib (@GRAPH_ATTRIBUTES) {
		$self->{$attrib} = $options->{$attrib} || $EMPTY_STRING;
	}
	#expect to add other code here
	return $self;
}

sub new {
	my ($class, $options) = @_;
	
	my $self = {};
  bless $self, $class;

  return $self->_initialize($options);

  #return $self;
}


#############################################################################
#Graph Method(s)                                                            #
#############################################################################

sub graph {
	my $self = shift;
	if (scalar(@_)) {
		my $options = shift;
		foreach my $attrib (@GRAPH_ATTRIBUTES) {
			$self->{$attrib} = $options->{$attrib} if exists($options->{$attrib});
		}
		return $self;
	}
	return( {label=>$self->{label},creator=>$self->{creator}} );
}

#############################################################################
#Node Methods                                                               #
#############################################################################

sub node {
	my ($self, $nodeParam) = @_;
	
	croak "node: missing nodeID / options parameter" if !defined($nodeParam);
	
	if (ref(\$nodeParam) eq 'SCALAR') {  # get call
		my $nodeID = $nodeParam;
	
		if (exists($self->{graph}{$nodeID})) {
			return( {id=>$nodeID, label=>$self->{graph}{$nodeID}{label}} );
		}
		return;
	}
	
	if (ref($nodeParam) eq 'HASH') {  #set call
		
		croak "node: missing \"id\" attribute in attributes hash" if !exists($nodeParam->{id});
		my $nodeID = $nodeParam->{id};
		croak "node: nodeID is not a SCALAR value" if ref(\$nodeID) ne 'SCALAR';
	
		foreach my $attrib (@NODE_ATTRIBUTES) {
			$self->{graph}{$nodeID}{$attrib} = $nodeParam->{$attrib} || $EMPTY_STRING;
		}
		return $self;
	}
	croak "node: invalid parameter: must be either a nodeID (simple scalar) or an attributes hash (reference)";
}

sub nodeExists {
	my ($self, $nodeID) = @_;
	
	croak "nodeExists: missing nodeID parameter" if !defined($nodeID);
	
	return (exists($self->{graph}{$nodeID})) ? 1 : 0;
}


sub nodeList {
	my $self = shift;
	
	my @nodeList = ();
	foreach my $node (keys %{$self->{graph}}) {
		push(@nodeList, { id=>$node, label=>$self->{graph}{$node}{label} } );
	}
	return @nodeList;
}


sub removeNode {
	my ($self, $nodeA) = @_;
	
	croak "removeNode: missing nodeID parameter" if !defined($nodeA);
	
	if (exists($self->{graph}{$nodeA})) {
		if (exists($self->{graph}{$nodeA}{edges})) {
			foreach my $nodeB (sort keys %{$self->{graph}{$nodeA}{edges}}) {
				delete($self->{graph}{$nodeB}{edges}{$nodeA});
			}
		}
		delete($self->{graph}{$nodeA});
		return $self;
	}
	return;
}

#############################################################################
#Edge Methods                                                               #
#############################################################################

		
sub edge {
	my ($self, $edgeHref) = @_;
	
	croak "edge: missing parameter hash reference" if !defined($edgeHref);
	croak "edge: parameter hash missing sourceID" if !exists($edgeHref->{sourceID});
	croak "edge: parameter hash missing targetID" if !exists($edgeHref->{targetID});
	
	my $sourceID = $edgeHref->{sourceID};
	my $targetID = $edgeHref->{targetID};
	
	if (defined( $edgeHref->{weight} )) {
		
		my $weight = $edgeHref->{weight};
		
		if ($weight <= 0) {
			carp "edge: invalid edge cost $sourceID $targetID $weight";
			return;
		}

		if ($sourceID eq $targetID) {
			carp "edge: source and target node IDs must be different";
			return;
		}
		
		$self->{graph}{$sourceID}{edges}{$targetID} = $weight;
		$self->{graph}{$targetID}{edges}{$sourceID} = $weight;
			
		return($self);
	}
	else {
		if (exists($self->{graph}{$sourceID}{edges}{$targetID})) {
			return( {sourceID=>$sourceID, targetID=>$targetID, weight=>$self->{graph}{$sourceID}{edges}{$targetID} } );
		}
	
		if (exists($self->{graph}{$sourceID}) and exists($self->{graph}{$targetID})) {
			return( {sourceID=>$sourceID, targetID=>$targetID, weight=>0} );
		}
		
		if (!exists($self->{graph}{$sourceID})) {
			carp "edge: sourceID $sourceID does not exist";
			return;
		}
		
		carp "edge: targetID $targetID does not exist";
		return;
	}
}


sub removeEdge {
	my ($self, $edgeHref) = @_;
	
	croak "removeEdge: missing parameter hash reference" if !defined($edgeHref);
	croak "removeEdge: parameter hash missing sourceID" if !exists($edgeHref->{sourceID});
	croak "removeEdge: parameter hash missing targetID" if !exists($edgeHref->{targetID});
	
	my $sourceID = $edgeHref->{sourceID};
	my $targetID = $edgeHref->{targetID};
		
	if (exists($self->{graph}{$sourceID}{edges}{$targetID})) {
		delete($self->{graph}{$sourceID}{edges}{$targetID});
		delete($self->{graph}{$targetID}{edges}{$sourceID});
		foreach my $node ($sourceID, $targetID) {
			my $hasNeighbors = 0;
			foreach my $neighbor (keys %{$self->{graph}{$node}{edges}}) {
				$hasNeighbors = 1;
				last;
			}
			if (!$hasNeighbors) {
				delete($self->{graph}{$node}{edges});
			}
		}
	}
	return $self;
}

	

sub edgeExists {
	my ($self, $edgeHref) = @_;
	
	croak "edgeExists: missing parameter hash reference" if !defined($edgeHref);
	croak "edgeExists: parameter hash missing sourceID" if !exists($edgeHref->{sourceID});
	croak "edgeExists: parameter hash missing targetID" if !exists($edgeHref->{targetID});
	
	my $sourceID = $edgeHref->{sourceID};
	my $targetID = $edgeHref->{targetID};
	
	return (exists($self->{graph}{$sourceID}{edges}{$targetID})) ? 1 : 0;
}


sub adjacent {
	my ($self, $edgeHref) = @_;
	
	croak "adjacent: missing parameter hash reference" if !defined($edgeHref);
	croak "adjacent: parameter hash missing sourceID" if !exists($edgeHref->{sourceID});
	croak "adjacent: parameter hash missing targetID" if !exists($edgeHref->{targetID});
	
	my $sourceID = $edgeHref->{sourceID};
	my $targetID = $edgeHref->{targetID};
	
	return ( exists($self->{graph}{$sourceID}{edges}{$targetID}) ) ? 1 : 0;
}


sub adjacentNodes {
	my ($self, $sourceID) = @_;
	
	if (!defined($sourceID)) {
		croak "adjacentNodes: missing node ID parameter";
	}
	
	my @neighbors = ();
	if (exists($self->{graph}{$sourceID}{edges})) {
		foreach my $targetID (sort keys %{$self->{graph}{$sourceID}{edges}}) {
			push(@neighbors, $targetID);
		}
	}
	return @neighbors;
}



#############################################################################
#Dijkstra Computation Methods                                               #
#############################################################################

#Computes Jordan center by creating all pairs shortest path matrix

sub vertexCenter {
	my ($self, $solutionMatrix) = @_;
	
	%$solutionMatrix = ();
	
	my @connectedNodeList = ();
	
	my $totalNodes = 0;
	foreach my $nodeID ( keys %{$self->{graph}} ) {
		$totalNodes++;
		next if !exists($self->{graph}{$nodeID}{edges});	#exclude disconnected nodes.
		push(@connectedNodeList, $nodeID);
	}
	print "graph contains ", scalar(@connectedNodeList), " connected nodes (nodes >= 1 edges)\n" if $VERBOSE;
	carp "excluded one or more disconnected nodes (nodes with no edges)" if (scalar(@connectedNodeList) < $totalNodes);
	
	foreach my $fromNodeID (@connectedNodeList) {
		
		$solutionMatrix->{rowMax}{$fromNodeID} = $PINF;
		
		foreach my $toNodeID (@connectedNodeList) {
			$solutionMatrix->{row}{$fromNodeID}{$toNodeID} = $PINF;
		}
		$solutionMatrix->{row}{$fromNodeID}{$fromNodeID} = 0;
	}
	
	
	my $cycle = 0;
	my $t0 = Benchmark->new;
	
	foreach my $origin (@connectedNodeList) {
		foreach my $destination (@connectedNodeList) {
			
			next if $solutionMatrix->{row}{$origin}{$destination} < $PINF or $origin eq $destination;
			#print "shortest path $origin -> $destination...";
			
			my $pq = Array::Heap::ModifiablePriorityQueue->new();	
			
			my %solution = ();
			my %unvisited = ();
			foreach my $node (@connectedNodeList) {
				$solution{$node}{cost} = $PINF;
			#	$solution{$node}{prevnode} = $EMPTY_STRING;  #not sure if this is needed
				$pq->add($node, $PINF);
			}
				
			$solution{$origin}{cost} = 0;
			$pq->add($origin,0); #modify weight of origin node
			
			
			#my $foundSolution = 0;
			while ($pq->size()) {
				$cycle++;
				
				my $visitNode = $pq->get();
				
				#setSolutionMatrix($solutionMatrix, $origin, $visitNode, $solution{$visitNode}{cost});
				$solutionMatrix->{row}{$origin}{$visitNode} = $solution{$visitNode}{cost};
				$solutionMatrix->{row}{$visitNode}{$origin} = $solution{$visitNode}{cost};
				
				last if ($visitNode eq $destination);
				
				foreach my $adjacentNode (keys %{$self->{graph}{$visitNode}{edges}}) {
					next if !defined($pq->weight($adjacentNode));
					
					my $thisCost = $solution{$visitNode}{cost} + $self->{graph}{$visitNode}{edges}{$adjacentNode};
					if ($thisCost < $solution{$adjacentNode}{cost}) {
						$solution{$adjacentNode}{cost} = $thisCost;
					#	$solution{$adjacentNode}{prevnode} = $visitNode;
						$pq->add($adjacentNode, $thisCost);
					}
				}
			}
				
			undef($pq);
		}
	}
	if ($VERBOSE) {
		my $t1 = Benchmark->new;
		#if ($cycle >= 1000) {
		#	print "\n";
		#}
		my $td = timediff($t1, $t0);
	  print "computing shortest path matrix took: ",timestr($td),"\n";
	}
	my $graphMinMax = $PINF;
	my $centerNode = '';
	foreach my $origin (@connectedNodeList) {
		my $rowMax = 0;
		foreach my $destination (@connectedNodeList) {
			next if $origin eq $destination;
			if ($solutionMatrix->{row}{$origin}{$destination} > $rowMax) {
				$rowMax = $solutionMatrix->{row}{$origin}{$destination};
			}
		}
		$solutionMatrix->{rowMax}{$origin} = $rowMax;
		if ($rowMax < $graphMinMax) {
			$graphMinMax = $rowMax;
		}
	}
	$solutionMatrix->{centerNodeSet} = [];
	if ($graphMinMax < $PINF) {
		foreach my $origin (@connectedNodeList) {
			if ($solutionMatrix->{rowMax}{$origin} == $graphMinMax) {
				push(@{$solutionMatrix->{centerNodeSet}}, $origin);
			}
		}
	}
	else {
		carp "Graph contains disconnected sub-graph. Center node set undefined.";
		$graphMinMax = 0;
	}
	#print "centernodeset ", join(',', @{$solutionMatrix->{centerNodeSet}}), "\n";
	return($graphMinMax);
}

sub farthestNode {  ## no critic (ProhibitExcessComplexity)
	my ($self, $solutionHref) = @_;
	
	if (!exists($solutionHref->{originID})) {
		croak "farthestNode: originID attribute not set in solution hash reference parameter";
	}
	my $originID = $solutionHref->{originID};
	
	if (!exists($self->{graph}{$originID})) {
		carp "farthestNode: originID not found: $originID";
		return 0;
	}
	elsif (!exists($self->{graph}{$originID}{edges})) {
		carp "farthestNode: origin node $originID has no edges";
		return 0;
	}
	my $pq = Array::Heap::ModifiablePriorityQueue->new();
	
	my %solution = ();		#initialize the solution hash
	my %unvisited = ();
	foreach my $node (keys %{$self->{graph}}) {
		if (exists($self->{graph}{$node}{edges})) {  #nodes without edges cannot be part of the solution
			$solution{$node}{weight} = $PINF;
			$solution{$node}{prevnode} = $EMPTY_STRING;
			$pq->add($node, $PINF);
		}
	}
		
	$solution{$originID}{weight} = 0;
	$pq->add($originID,0); #modify weight of origin node
	
	my $cycle = 0;
	my $t0 = Benchmark->new;
	
	while ($pq->size()) {
		$cycle++;
		#print '.' if $VERBOSE and ($cycle % 1000 == 0);
		
		my $visitNode = $pq->get();
		
		foreach my $adjacentNode (keys %{$self->{graph}{$visitNode}{edges}}) {
			next if !defined($pq->weight($adjacentNode));
			
			my $thisWeight = $solution{$visitNode}{weight} + $self->{graph}{$visitNode}{edges}{$adjacentNode};
			if ($thisWeight < $solution{$adjacentNode}{weight}) {
				$solution{$adjacentNode}{weight} = $thisWeight;
				$solution{$adjacentNode}{prevnode} = $visitNode;
				$pq->add($adjacentNode, $thisWeight);
			}
		}
	}
	if ($VERBOSE) {
		my $t1 = Benchmark->new;
		#if ($cycle >= 1000) {
		#	print "\n";
		#}
		my $td = timediff($t1, $t0);
	  print "dijkstra's algorithm took: ",timestr($td),"\n";
	}
  
	my $farthestWeight = 0;
	foreach my $node (sort keys %solution) {
		if ($solution{$node}{weight} < $PINF and $solution{$node}{weight} > $farthestWeight) {
			$farthestWeight = $solution{$node}{weight};
			#$farthestnode = $node;
		}
	}
	
	my $solutioncnt = 0;
	%{$solutionHref} = (
		desc => 'farthest',
		originID => $originID,
		weight => $farthestWeight,
	);
	
	foreach my $farthestnode (sort keys %solution) {
		if ($solution{$farthestnode}{weight} == $farthestWeight) {
			
			$solutioncnt++;
			
			print "\nfarthestNode: (solution $solutioncnt) farthest node from origin $originID is $farthestnode at weight (cost) $farthestWeight\n" if $VERBOSE;
			
			my $fromNode = $solution{$farthestnode}{prevnode};
			my @path = ( $farthestnode, $fromNode );
			
			my %loopCheck = ();
			while ($solution{$fromNode}{prevnode} ne $EMPTY_STRING) {
				$fromNode = $solution{$fromNode}{prevnode};
				if (exists($loopCheck{$fromNode})) {
					print "farthestNode: path loop at $fromNode\n";
					print 'farthestNode: path = ', join(',',@path), "\n";
					die 'farthestNode internal error: destination to origin path logic error';
				}
				$loopCheck{$fromNode} = 1;
				push(@path,$fromNode);
			}
			
			@path = reverse(@path);
			
			my $nexttolast = $#path - 1;
			
			$solutionHref->{path}{$solutioncnt}{destinationID} = $farthestnode;
			$solutionHref->{path}{$solutioncnt}{edges} = [];
				
			foreach my $i (0 .. $nexttolast) {
				
				push(@{$solutionHref->{path}{$solutioncnt}{edges}}, {sourceID => $path[$i], targetID => $path[$i+1], weight => $self->edge( { sourceID=>$path[$i], targetID=>$path[$i+1] } )->{weight} } );
				
			}
		}
	}

	$solutionHref->{count} = $solutioncnt;
	
	return($farthestWeight);
}

sub shortestPath { ## no critic (ProhibitExcessComplexity)
	my ($self, $solutionHref) = @_;
	
	if (!exists($solutionHref->{originID})) {
		croak "farthestNode: originID attribute not set in solution hash reference parameter";
	}
	my $originID = $solutionHref->{originID};
	
	if (!exists($solutionHref->{destinationID})) {
		croak "farthestNode: destinationID attribute not set in solution hash reference parameter";
	}
	my $destinationID = $solutionHref->{destinationID};
	
	if (!exists($self->{graph}{$originID})) {
		carp "shortestPath: originID not found: $originID";
		return 0;
	}
	
	if (!exists($self->{graph}{$originID}{edges})) {
		carp "shortestPath: origin node $originID has no edges";
		return 0;
	}
	if (!exists($self->{graph}{$destinationID})) {
		carp "shortestPath: destinationID not found: $destinationID";
		return 0;
	}
	if (!exists($self->{graph}{$destinationID}{edges})) {
		carp "shortestPath: destination node $destinationID has no edges";
		return 0;
	}
	
	my $pq = Array::Heap::ModifiablePriorityQueue->new();
	
	my %solution = ();		#initialize the solution hash
	my %unvisited = ();
	foreach my $node (keys %{$self->{graph}}) {
		if (exists($self->{graph}{$node}{edges})) {  #nodes without edges cannot be part of the solution
			$solution{$node}{weight} = $PINF;
			$solution{$node}{prevnode} = $EMPTY_STRING;
			$pq->add($node, $PINF);
		}
	}
		
	$solution{$originID}{weight} = 0;
	$pq->add($originID,0); #modify weight of origin node
	
	my $cycle = 0;
	my $t0 = Benchmark->new;
	
	my $foundSolution = 0;
	while ($pq->size()) {
		$cycle++;
		#print '.' if $VERBOSE and ($cycle % 1000 == 0);
		
		my $visitNode = $pq->get();
		
		if ($visitNode eq $destinationID) {
			$foundSolution = 1 if $solution{$visitNode}{weight} < $PINF;
			last;
		}
		
		foreach my $adjacentNode (keys %{$self->{graph}{$visitNode}{edges}}) {
			next if !defined($pq->weight($adjacentNode));
			
			my $thisWeight = $solution{$visitNode}{weight} + $self->{graph}{$visitNode}{edges}{$adjacentNode};
			if ($thisWeight < $solution{$adjacentNode}{weight}) {
				$solution{$adjacentNode}{weight} = $thisWeight;
				$solution{$adjacentNode}{prevnode} = $visitNode;
				$pq->add($adjacentNode, $thisWeight);
			}
		}
	}
	if ($VERBOSE) {
		my $t1 = Benchmark->new;
		#if ($cycle >= 1000) {
		#	print "\n";
		#}
		my $td = timediff($t1, $t0);
	  print "dijkstra's algorithm took: ",timestr($td),"\n";
	}
  
  my $pathWeight = 0;
  if ($foundSolution) {
	  $pathWeight = $solution{$destinationID}{weight};
		print "shortestPath: pathWeight (cost) = $pathWeight\n" if $VERBOSE;
		
		my $solutioncnt = 0;
		%{$solutionHref} = (
			desc => 'path',
			originID => $originID,
			destinationID => $destinationID,
			weight => $pathWeight,
		);
		
		my $fromNode = $solution{$destinationID}{prevnode};
		my @path = ( $destinationID, $fromNode );
		
		my %loopCheck = ();
		while ($solution{$fromNode}{prevnode} ne $EMPTY_STRING) {
			$fromNode = $solution{$fromNode}{prevnode};
			if (exists($loopCheck{$fromNode})) {
				print "shortestPath: path loop at $fromNode\n";
				print "shortestPath: path = ", join(',',@path), "\n";
				die "shortestPath internal error: destination to origin path logic error";
			}
			$loopCheck{$fromNode} = 1;
			push(@path,$fromNode);
		}
		
		@path = reverse(@path);
		
		my $nexttolast = $#path - 1;
		foreach my $i (0 .. $nexttolast) {
			push(@{$solutionHref->{edges}}, {sourceID => $path[$i], targetID => $path[$i+1], weight => $self->edge( { sourceID=>$path[$i], targetID=>$path[$i+1] } )->{weight} } );
		}
	}
	return($pathWeight);
}



#############################################################################
#input / output file methods                                                #
#############################################################################

{ #CSV file format methods

	use Text::CSV_XS;
	
	sub inputGraphfromCSV {
		my ($self, $filename) = @_;
		
		my $nodecount = 0;
		my $edgecount = 0;
		
		open(my $infile, '<:encoding(UTF-8)', $filename) or croak "could not open '$filename'";
		
		print "inputGraphfromCSV: opened '$filename' for input\n" if $VERBOSE;
	
		my $csv = Text::CSV_XS->new ({ binary => 1, auto_diag => 1 });
		while (my $row = $csv->getline ($infile)) {
			if (lc($row->[0]) eq 'node') {
				$self->node({id=>$row->[1], label=>$row->[2]} );
				$nodecount++;
			}
			elsif (lc($row->[0]) eq 'edge') {
				$self->edge( { sourceID=>$row->[1], targetID=>$row->[2], weight=>$row->[3] } );
				$edgecount++;
			}
		}
		close($infile);
		
		carp "inputGraphfromCSV: no nodes read from '$filename'" if !$nodecount;
		carp "inputGraphfromCSV: no edges read from '$filename'" if !$edgecount;
		
		print "inputGraphfromCSV: found $nodecount nodes and $edgecount edges\n" if $VERBOSE;
		return $self;
	}
	
	sub outputGraphtoCSV {
		my ($self, $filename) = @_;
		
		open(my $outfile, '>:encoding(UTF-8)', $filename) or croak "could not open '$filename'";
		
		print "outputGraphtoCSV: opened '$filename' for output\n" if $VERBOSE;
		
		my $csv = Text::CSV_XS->new ({ binary => 1, auto_diag => 1 });
		
		my $nodecount = 0;
		my $edgecount = 0;
		
		my %edges = ();
		foreach my $nodeID (keys %{$self->{graph}}) {
			
			$csv->say($outfile, ['node', $nodeID, $self->{graph}{$nodeID}{label} ]);
			
			$nodecount++;
			if (exists($self->{graph}{$nodeID}{edges})) {
				foreach my $targetID (keys %{$self->{graph}{$nodeID}{edges}}) {
					if (!exists($edges{$targetID}{$nodeID})) {
						$edges{$nodeID}{$targetID} = $self->{graph}{$nodeID}{edges}{$targetID};
					}
				}
			}
		}
		foreach my $sourceID (keys %edges) {
			foreach my $targetID (keys %{$edges{$sourceID}}) {
				
				$csv->say($outfile, ['edge', $sourceID, $targetID, $edges{$sourceID}{$targetID} ]);
			
				$edgecount++;
			}
		}
		close($outfile);
		print "outputGraphtoCSV: wrote $nodecount nodes and $edgecount edges to '$filename'\n" if $VERBOSE;
		
		return $self;
	}
	
	sub outputAPSPmatrixtoCSV {
		my ($either, $solutionMatrix, $filename, $labelSort) = @_;
		
		$labelSort = '' if !defined($labelSort);
		
		open(my $outfile, '>:encoding(UTF-8)', $filename) or croak "could not open '$filename'";
		
		print "outputAPSPmatrixtoCSV: opened '$filename' for output\n" if $VERBOSE;
		
		my $csv = Text::CSV_XS->new ({ binary => 1, auto_diag => 1 });
		
		my @nodeList = (lc($labelSort) eq 'numeric') ? (sort {$a <=> $b} keys %{$solutionMatrix->{row}}) : (sort keys %{$solutionMatrix->{row}});
		
		$csv->say($outfile, ['From/To', @nodeList ]);
		my $rowcount = 1;
		
		foreach my $nodeID (@nodeList) {
			my @row = ();
			foreach my $destinationID (@nodeList) {
				push(@row, $solutionMatrix->{row}{$nodeID}{$destinationID});
			}
			$csv->say($outfile, [$nodeID, @row]);
			$rowcount++;
		}
		close($outfile);
		print "outputAPSPmatrixtoCSV: wrote $rowcount rows to '$filename'\n" if $VERBOSE;
		return $either;
		
	}
	
} #CSV file format I/O methods

#############################################################################
#JSON Graph Specification file format methods                               #
#############################################################################
{ 
		
	use JSON;

	sub inputGraphfromJSON {
		my ($self, $filename) = @_;
		
		my $json_text = $EMPTY_STRING;
		open(my $infile, '<:encoding(UTF-8)', $filename) or croak "could not open '$filename'";
		
		print "inputGraphfromJSON: opened '$filename' for input\n" if $VERBOSE;
		
		while (my $line = <$infile>) {
			$json_text .= $line;
		}
		close($infile);
	
		my $graphHref = from_json( $json_text, {utf8 => 1} ) or croak "inputGraphfromJSON: invalid json text";
		
		if (ref($graphHref) ne 'HASH') {
			croak "inputGraphfromJSON: invalid JSON text";
		}
		
		if (exists($graphHref->{graphs})) {
			croak "inputGraphfromJSON: JSON \"multi graph\" type not supported";
		}
		if (!exists($graphHref->{graph}{edges})) {
			croak "inputGraphfromJSON: not a JSON graph specification or graph has no edges";
		}
		
		if (exists($graphHref->{graph}{directed}) and $graphHref->{graph}{directed} ) {
			croak "inputGraphfromJSON: graph attribute \"directed\" is true (directed).  Not supported.";
		}
		$self->graph( {label=>$graphHref->{graph}{label} } ) if exists($graphHref->{graph}{label});
		$self->graph( {creator=>$graphHref->{graph}{metadata}{creator} } ) if exists($graphHref->{graph}{metadata}{creator});
		
		my $nodecount = 0;
		my $edgecount = 0;
		
		foreach my $nodeHref (@{$graphHref->{graph}{nodes}}) {
			$nodecount++;
			$self->node( {id=>$nodeHref->{id}, label=>$nodeHref->{label} } );
		}
		foreach my $edgeHref (@{$graphHref->{graph}{edges}}) {
			if (exists($edgeHref->{directed}) and $edgeHref->{directed}) {
				croak "inputGraphfromJSON: directed edge $edgeHref->{source}, $edgeHref->{target}";
			}
			$edgecount++;
			$self->edge( { sourceID=>$edgeHref->{source}, targetID=>$edgeHref->{target}, weight=>$edgeHref->{metadata}{value} } );
		}
		
		carp "inputGraphfromJSON: no nodes read from '$filename'" if !$nodecount;
		carp "inputGraphfromJSON: no edges read from '$filename'" if !$edgecount;
		
		print "inputGraphfromJSON: found $nodecount nodes and $edgecount edges\n" if $VERBOSE;
		
		return $self;
	}
	
	
	sub outputGraphtoJSON {
		my ($self, $filename) = @_;
		
		my $nodecount = 0;
		my $edgecount = 0;
		
		my %graph = ();
		$graph{graph}{directed} = JSON::false;
		@{$graph{graph}{nodes}} = ();
		@{$graph{graph}{edges}} = ();
		
		$graph{graph}{metadata}{comment} = 'generated by Graph::Dijkstra on ' . localtime;
		$graph{graph}{label} = $self->{label} if $self->{label};
		$graph{graph}{metadata}{creator} = $self->{creator} if $self->{creator};

			 
		my %edges = ();
		foreach my $nodeID (keys %{$self->{graph}}) {
			
			push(@{$graph{graph}{nodes}}, { id => $nodeID, label => $self->{graph}{$nodeID}{label} } );
			
			$nodecount++;
			if (exists($self->{graph}{$nodeID}{edges})) {
				foreach my $targetID (keys %{$self->{graph}{$nodeID}{edges}}) {
					if (!exists($edges{$targetID}{$nodeID})) {
						$edges{$nodeID}{$targetID} = $self->{graph}{$nodeID}{edges}{$targetID};
						push( @{$graph{graph}{edges}}, { source => $nodeID, target => $targetID, metadata => {value => $self->{graph}{$nodeID}{edges}{$targetID} } } );
						$edgecount++;
					}
				}
			}
		}
		
		my $json_text = to_json(\%graph, {utf8 => 1, pretty => 1});
		
		open(my $outfile, '>:encoding(UTF-8)', $filename) or croak "could not open '$filename'";
		
		print "outputGraphtoJSON: opened '$filename' for output\n" if $VERBOSE;
		print {$outfile} $json_text;
		close($outfile);
		print "outputGraphtoJSON: wrote $nodecount nodes and $edgecount edges to '$filename'\n" if $VERBOSE;
		
		return $self;
	}
	
} #JSON Graph Specification file format methods

#############################################################################
#GML file format methods                                                    #
#############################################################################
{  
	
	use Regexp::Common;
	use HTML::Entities qw(encode_entities);

	sub inputGraphfromGML { ## no critic (ProhibitExcessComplexity)
		my ($self, $filename) = @_;
		
		my $buffer = $EMPTY_STRING;
		my $linecount = 0;
		open(my $infile, '<:encoding(UTF-8)', $filename) or croak "could not open '$filename'";
		
		print "inputGraphfromGML: opened '$filename' for input\n" if $VERBOSE;
		
		while (my $line = <$infile>) {
			next if substr($line,0,1) eq '#';
			$buffer .= $line;
			$linecount++;
		}
		close($infile);
		print "inputGraphfromGML: read $linecount lines\n" if $VERBOSE;
		
		if ($buffer !~ /graph\s+\[.+?(?:node|edge)\s+\[/ixs) {
			croak "file does not appear to be GML format";
		}
		
		if ($buffer =~ /graph\s+\[\s+directed\s+(\d)/ixs) {
			my $directed = $1;
			print "inputGraphfromGML: graph directed = '$directed'\n" if $VERBOSE;
			if ($directed) {
				croak "graph type is directed.  Not supported."
			}
		}
		
		if ($buffer =~ /^\s*creator\s+\"([^\"]+)\"/i) {
			my $creator = $1;
			$self->graph( {creator=>$creator} );
			print "inputGraphfromGML: graph attribute creator set: $creator\n" if $VERBOSE;
			
		}
		
		my $has_graphics_elements = ($buffer =~ /graphics\s+\[/) ? 1 : 0;
		print "GML file contain graphics elements\n" if ($VERBOSE and $has_graphics_elements);
		
		my $balancedRE = $RE{balanced}{-parens=>'[]'};
		
		
		my $nodecount = 0;
		my $edgecount = 0;
		
		my %keyvals = ();
		while ($buffer =~ /(node|edge)\s+$balancedRE/gixso) {
			my $type = lc($1);
			my $attribs = $2;
			#my $bufferPos = $-[0];
			
			$attribs = substr($attribs, 1, -1);
		
			$attribs =~ s/graphics\s+$balancedRE//xio if $has_graphics_elements and $type eq 'node';
			
			while ($attribs =~/(id|label|source|target|value)\s+(?|([0-9\.]+)|\"([^\"]+)\")/gixs) {
				my $attrib = lc($1);
				my $attribValue = $2;
				if ($type eq 'edge' and $attrib eq 'value' and !looks_like_number($attribValue)) {
					carp "non-numeric edge value '$attribValue'.  Skipped.";
					next;
				}
				$keyvals{$attrib} = $attribValue;
			}
	
			if ($type eq 'node') {
				$nodecount++;
				if (exists($keyvals{id})) {
					$self->{graph}{$keyvals{id}}{label} = $keyvals{label} || $EMPTY_STRING;
				}
				else {
					croak "inputGraphfromGML: node: missing id problem -- matched attribs: '$attribs'";
				}
			}
			else {
				$edgecount++;
				if (exists($keyvals{source}) and exists($keyvals{target}) and exists($keyvals{value}) and $keyvals{value} > 0) {
					$self->edge( { sourceID=>$keyvals{source}, targetID=>$keyvals{target}, weight=>$keyvals{value} } );
				}
				else {
					croak "inputGraphfromGML: edge: missing source, target, value, or value <= 0 problem -- matched attribs '$attribs'";
				}
			}
		}
	
		carp "inputGraphfromGML: no nodes read from '$filename'" if !$nodecount;
		carp "inputGraphfromGML: no edges read from '$filename'" if !$edgecount;
		
		print "inputGraphfromGML: found $nodecount nodes and $edgecount edges\n" if $VERBOSE;
		
		return $self;
	}


	sub outputGraphtoGML {
		my ($self, $filename) = @_;
		
		open(my $outfile, '>:encoding(UTF-8)', $filename) or croak "could not open '$filename' for output";
		
		print "outputGraphtoGML: opened '$filename' for output\n" if $VERBOSE;
		
		{
			my $now_string = localtime;
			print {$outfile} "# Generated by Graph::Dijkstra on $now_string\n";
		}
		
		print {$outfile} "Creator \"$self->{creator}\"\n" if $self->{creator};
		print {$outfile} "Graph [\n\tDirected 0\n";
		
		my $nodecount = 0;
		my $edgecount = 0;
		
		my %edges = ();
		foreach my $nodeID (keys %{$self->{graph}}) {
			my $nodeIDprint = (looks_like_number($nodeID)) ? $nodeID : '"' . encode_entities($nodeID) . '"';
			my $nodeLabel = encode_entities($self->{graph}{$nodeID}{label});
			print {$outfile} "\tnode [\n\t\tid $nodeIDprint\n\t\tlabel \"$nodeLabel\"\n\t]\n";
			$nodecount++;
			if (exists($self->{graph}{$nodeID}{edges})) {
				foreach my $targetID (keys %{$self->{graph}{$nodeID}{edges}}) {
					if (!exists($edges{$targetID}{$nodeID})) {
						$edges{$nodeID}{$targetID} = $self->{graph}{$nodeID}{edges}{$targetID};
					}
				}
			}
		}
		foreach my $sourceID (keys %edges) {
			foreach my $targetID (keys %{$edges{$sourceID}}) {
				my $sourceIDprint = (looks_like_number($sourceID)) ? $sourceID : '"' . encode_entities($sourceID) . '"';
				my $targetIDprint = (looks_like_number($targetID)) ? $targetID : '"' . encode_entities($targetID) . '"';
				print {$outfile} "\tedge [\n\t\tsource $sourceIDprint\n\t\ttarget $targetIDprint\n\t\tvalue $edges{$sourceID}{$targetID}\n\t]\n";
				$edgecount++;
			}
		}
		print {$outfile} "]\n";
		close($outfile);
		print "outputGraphtoGML: wrote $nodecount nodes and $edgecount edges to '$filename'\n" if $VERBOSE;
		
		return $self;
	}

} #GML file format methods

#############################################################################
#XML file format methods: GraphML and GEXF                                  #
#############################################################################
{  

	use XML::LibXML;
	
	sub inputGraphfromGraphML { ## no critic (ProhibitExcessComplexity)
		my ($self, $filename, $options) = @_;
		
		
		my $dom = XML::LibXML->load_xml(location => $filename);
		
		print "inputGraphfromGraphML: input '$filename'\n" if $VERBOSE;
		
		my $topNode = $dom->nonBlankChildNodes()->[0];
		
		croak "inputGraphfromGraphML: not a GraphML format XML file" if lc($topNode->nodeName()) ne 'graphml';
		
		my $nsURI = $topNode->getAttribute('xmlns') || '';
		
		croak "inputGraphfromGraphML: not a GraphML format XML file" if (lc($nsURI) ne 'http://graphml.graphdrawing.org/xmlns');
		
		my $xpc = XML::LibXML::XPathContext->new($dom);
		$xpc->registerNs('gml', $nsURI);
		
		my $labelKey = $options->{nodeKeyLabelID} || $EMPTY_STRING;
		my $weightKey = $options->{edgeKeyValueID} || $EMPTY_STRING;
		
		my $defaultWeight = 0;
		
		my $nodecount = 0;
		my $edgecount = 0;
		
		if (my $graphNode = $xpc->findnodes('/gml:graphml/gml:graph')->[0] ) {
			my $directed = $graphNode->getAttribute('edgedefault') || $EMPTY_STRING;
			croak "inputGraphfromGraphML: graph edgedefault is 'directed'.  Not supported." if $directed eq 'directed';
		}
		else {
			croak "inputGraphfromGraphML: GraphML file has no <graph> element";
		}
		
		if (my $graphNode = $xpc->findnodes('/gml:graphml/gml:graph[2]')->[0] ) {
			croak "inputGraphfromGraphML: file contains more than one graph.  Not supported.";
		}
		
		if (my $graphNode = $xpc->findnodes('/gml:graphml/gml:graph/gml:node/gml:graph')->[0] ) {
			croak "inputGraphfromGraphML: file contains one or more embedded graphs.  Not supported.";
		}
		
		if ($weightKey) {
			if (my $keyWeightNode = $xpc->findnodes("/gml:graphml/gml:key[\@for=\"edge\" and \@id=\"$weightKey\"]")->[0]) {
				print "inputGraphfromGraphML: found edgeKeyValueID '$weightKey' in GraphML key elements list\n" if $VERBOSE;
				if (my $defaultNode = $xpc->findnodes('.//gml:default[1]',$keyWeightNode)->[0]) {
					$defaultWeight = $defaultNode->textContent();
				}
			}
			else {
				carp "inputGraphfromGraphML: edgeKeyValueID '$weightKey' not found in GraphML key elements list";
				$weightKey = $EMPTY_STRING;
			}
		}
		
		if (!$weightKey) {
			foreach my $keyEdge ($xpc->findnodes('/gml:graphml/gml:key[@for="edge"]') ) {
				my $attrName = $keyEdge->getAttribute('attr.name');
				if ($IS_GRAPHML_WEIGHT_ATTR{ lc($attrName) } ) {
					$weightKey = $keyEdge->getAttribute('id');
					print "inputGraphfromGraphML: found key attribute for edge attr.name='$attrName' id='$weightKey'\n" if $VERBOSE;
					if (my $defaultNode = $xpc->findnodes('.//gml:default[1]',$keyEdge)->[0]) {
						$defaultWeight = $defaultNode->textContent();
					}
					last;
				}
			}
			
			if (!$weightKey) {
				croak "inputGraphfromGraphML: graph does not contain key attribute for edge weight/value/cost/distance '<key id=\"somevalue\" for=\"edge\" attr.name=\"weight|value|cost|distance\" />'.  Not supported.";
			}
		}
		
		my $labelXPATH = $EMPTY_STRING;
		
		if ($labelKey) {
			if (my $keyNodeLabelNode = $xpc->findnodes("/gml:graphml/gml:key[\@for=\"node\" and \@id=\"$labelKey\"]")->[0]) {
				print "inputGraphfromGraphML: found nodeLabelValueID '$labelKey' in GraphML key elements list\n" if $VERBOSE;
			}
			else {
				carp "inputGraphfromGraphML: nodeLabelValueID '$labelKey' not found in GraphML key elements list";
				$labelKey = $EMPTY_STRING;
			}
		}
		
		if (!$labelKey) {
			foreach my $keyNode ($xpc->findnodes('/gml:graphml/gml:key[@for="node" and @attr.type="string"]')) {
				my $attrName = $keyNode->getAttribute('attr.name') || $EMPTY_STRING;
				if ($IS_GRAPHML_LABEL_ATTR{lc($attrName)}) {
					$labelKey = $keyNode->getAttribute('id');
					print "inputGraphfromGraphML: found key attribute for node 'label' attr.name='$attrName' id='$labelKey'\n" if $VERBOSE;
					last;
				}
			}
		}
		
		if (!$labelKey) {
			carp "inputGraphfromGraphML: key node name / label / description attribute not found in graphml";
		}
		else {
			$labelXPATH = ".//gml:data[\@key=\"$labelKey\"]";
		}
		
		if (my $keyGraphCreator = $xpc->findnodes("/gml:graphml/gml:key[\@for=\"graph\" and \@id=\"creator\"]")->[0]) {
			if (my $dataGraphCreator = $xpc->findnodes("/gml:graphml/gml:graph/gml:data[\@key=\"creator\"]")->[0]) {
				if (my $creator = $dataGraphCreator->textContent()) {
					$self->graph( {creator=>$creator} );
				}
			}
		}
		if (my $keyGraphLabel = $xpc->findnodes("/gml:graphml/gml:key[\@for=\"graph\" and \@id=\"label\"]")->[0]) {
			if (my $dataGraphLabel = $xpc->findnodes("/gml:graphml/gml:graph/gml:data[\@key=\"label\"]")->[0]) {
				if (my $label = $dataGraphLabel->textContent()) {
					$self->graph( {label=>$label} );
				}
			}
		}
		
		
		foreach my $nodeElement ($xpc->findnodes('/gml:graphml/gml:graph/gml:node')) {
			
			my $node = $nodeElement->nodeName();
			my $id = $nodeElement->getAttribute('id');
			my $label = $EMPTY_STRING;
			if ($labelXPATH and my $dataNameNode = $xpc->findnodes($labelXPATH,$nodeElement)->[0]) {
				$label = $dataNameNode->textContent();
			}
			$self->node( {id=>$id,label=>$label } );
			$nodecount++;
		}
		
		my $weightXPATH = ".//gml:data[\@key=\"$weightKey\"]";
		
		foreach my $edgeElement ($xpc->findnodes('/gml:graphml/gml:graph/gml:edge')) {
			
			my $edge = $edgeElement->nodeName();
			my $source = $edgeElement->getAttribute('source') || $EMPTY_STRING;
			my $target = $edgeElement->getAttribute('target') || $EMPTY_STRING;
			my $weight = $defaultWeight;
			if (my $dataWeightNode = $xpc->findnodes($weightXPATH,$edgeElement)->[0]) {
				$weight = $dataWeightNode->textContent();
			}
			if ($weight) {
				$self->edge( { sourceID=>$source, targetID=>$target, weight=>$weight } );
				$edgecount++;
			}
			else {
				carp "inputGraphfromGraphML: edge $source $target has no weight. Not created."
			}
		
		}
		
		carp "inputGraphfromGraphML: no nodes read from '$filename'" if !$nodecount;
		carp "inputGraphfromGraphML: no edges read from '$filename'" if !$edgecount;
		
		print "inputGraphfromGraphML: found $nodecount nodes and $edgecount edges\n" if $VERBOSE;
		
		return $self;
	}
	
	
	sub outputGraphtoGraphML {
		my ($self, $filename, $options) = @_;
		
		my $nsURI = "http://graphml.graphdrawing.org/xmlns";
		
		my $doc = XML::LibXML::Document->new('1.0','UTF-8');
		my $graphML = $doc->createElementNS( $EMPTY_STRING, 'graphml' );
		$doc->setDocumentElement( $graphML );
	 
		$graphML->setNamespace( $nsURI , $EMPTY_STRING, 1 );
		
		{
			my $now_string = localtime;
			$graphML->appendChild($doc->createComment("Generated by Graph::Dijkstra on $now_string"));
		}
		
		$graphML->setAttribute('xmlns:xsi','http://www.w3.org/2001/XMLSchema-instance');
		$graphML->setAttribute('xsi:schemaLocation','http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd');
		
		
		
		my $keyEdgeWeightID = $options->{keyEdgeWeightID} || 'weight';
		my $keyEdgeWeightAttrName = $options->{keyEdgeWeightAttrName} || 'weight';
		my $keyNodeLabelID = $options->{keyNodeLabelID} || 'name';
		my $keyNodeLabelAttrName = $options->{keyNodeLabelAttrName} || 'name';
		
		my $keyNode = $graphML->addNewChild( $nsURI, 'key' );
		
		$keyNode->setAttribute('for','node');
		$keyNode->setAttribute('id', $keyNodeLabelID );
		$keyNode->setAttribute('attr.name', $keyNodeLabelAttrName );
		$keyNode->setAttribute('attr.type', 'string' );
		
		my $keyEdge = $graphML->addNewChild( $nsURI, 'key' );
		$keyEdge->setAttribute('for','edge');
		$keyEdge->setAttribute('id', $keyEdgeWeightID );
		$keyEdge->setAttribute('attr.name', $keyEdgeWeightAttrName );
		$keyEdge->setAttribute('attr.type', 'double' );
		
		if ($self->{creator}) {
			my $keyGraph = $graphML->addNewChild( $nsURI, 'key' );
			$keyGraph->setAttribute('for','graph');
			$keyGraph->setAttribute('id','creator');
			$keyGraph->setAttribute('attr.name','creator');
			$keyGraph->setAttribute('attr.type','string');
		}
		if ($self->{label}) {
			my $keyGraph = $graphML->addNewChild( $nsURI, 'key' );
			$keyGraph->setAttribute('for','graph');
			$keyGraph->setAttribute('id','label');
			$keyGraph->setAttribute('attr.name','label');
			$keyGraph->setAttribute('attr.type','string');
		}
		
		my $graph = $graphML->addNewChild( $nsURI, 'graph' );
		$graph->setAttribute('id','G');
		$graph->setAttribute('edgedefault','undirected');
		if ($self->{creator}) {
			my $dataNode = $graph->addNewChild( $nsURI, 'data');
			$dataNode->setAttribute('key', 'creator');
			$dataNode->appendTextNode( $self->{creator} );
		}
		if ($self->{label}) {
			my $dataNode = $graph->addNewChild( $nsURI, 'data');
			$dataNode->setAttribute('key', 'label');
			$dataNode->appendTextNode( $self->{label} );
		}
		
		my $nodecount = 0;
		my $edgecount = 0;
		
		my %edges = ();
		foreach my $nodeID (keys %{$self->{graph}}) {
			
			my $nodeNode = $graph->addNewChild( $nsURI, 'node' );
			$nodeNode->setAttribute('id', $nodeID);
			my $dataNode = $nodeNode->addNewChild( $nsURI, 'data');
			$dataNode->setAttribute('key', $keyNodeLabelID);
			$dataNode->appendTextNode( $self->{graph}{$nodeID}{label} );
			
			$nodecount++;
			if (exists($self->{graph}{$nodeID}{edges})) {
				foreach my $targetID (keys %{$self->{graph}{$nodeID}{edges}}) {
					if (!exists($edges{$targetID}{$nodeID})) {
						$edges{$nodeID}{$targetID} = $self->{graph}{$nodeID}{edges}{$targetID};
					}
				}
			}
		}
		foreach my $sourceID (keys %edges) {
			foreach my $targetID (keys %{$edges{$sourceID}}) {
				
				$edgecount++;
				my $edgeNode = $graph->addNewChild( $nsURI, 'edge');
				$edgeNode->setAttribute('id', $edgecount);
				$edgeNode->setAttribute('source', $sourceID );
				$edgeNode->setAttribute('target', $targetID );
				my $dataNode = $edgeNode->addNewChild( $nsURI, 'data');
				$dataNode->setAttribute('key', $keyEdgeWeightID );
				$dataNode->appendTextNode( $edges{$sourceID}{$targetID} );
				
			}
		}
		
		my $state = $doc->toFile($filename,2);
		croak "could not output internal grap to '$filename'" if !$state;
		
		print "outputGraphtoGraphML: wrote $nodecount nodes and $edgecount edges to '$filename'\n" if $VERBOSE;
		return $self;
	}
	
	
	sub inputGraphfromGEXF { ## no critic (ProhibitExcessComplexity)
		my ($self, $filename) = @_;
		
		
		my $dom = XML::LibXML->load_xml(location => $filename);
		
		print "inputGraphfromGEXF: input '$filename'\n" if $VERBOSE;
		
		my $topNode = $dom->nonBlankChildNodes()->[0];
		
		croak "inputGraphfromGEXF: not a GEXF format XML file" if lc($topNode->nodeName()) ne 'gexf';
		
		my $nsURI = $topNode->getAttribute('xmlns') || '';
		
		croak "inputGraphfromGEXF: not a GEXF draft specification 1.1 or 1.2 format XML file" if ( $nsURI !~ /^http:\/\/www.gexf.net\/1\.[1-2]draft$/i );
		
		my $xpc = XML::LibXML::XPathContext->new($dom);
		$xpc->registerNs('gexf', $nsURI);
						
		my $nodecount = 0;
		my $edgecount = 0;
		my $defaultWeight = 1;
		
		if (my $graphNode = $xpc->findnodes('/gexf:gexf/gexf:graph')->[0] ) {
			my $directed = $graphNode->getAttribute('defaultedgetype') || $EMPTY_STRING;
			croak "inputGraphfromGEXF: graph edgedefault is 'directed'.  Not supported." if lc($directed) eq 'directed';
			my $mode = $graphNode->getAttribute('mode') || $EMPTY_STRING;
			carp "inputGraphfromGEXF: graph mode is 'dynamic'.  Not supported." if lc($mode) eq 'dynamic';
		}
		else {
			croak "inputGraphfromGEXF: GEXF file has no <graph> element";
		}
		
		if (my $graphNode = $xpc->findnodes('/gexf:gexf/gexf:graph[2]')->[0] ) {
			croak "inputGraphfromGEXF: file contains more than one graph.  Not supported.";
		}
		
		if (my $heirarchyNode = $xpc->findnodes('/gexf:gexf/gexf:graph/gexf:nodes/gexf:node/gexf:nodes')->[0] ) {
			croak "inputGraphfromGEXF: file contains heirarchical nodes.  Not supported.";
		}
		if (my $parentsNode = $xpc->findnodes('/gexf:gexf/gexf:graph/gexf:nodes/gexf:node/gexf:parents')->[0] ) {
			croak "inputGraphfromGEXF: file contains parent nodes.  Not supported.";
		}
		
		if (my $metaNode = $xpc->findnodes('/gexf:gexf/gexf:meta/gexf:creator')->[0] ) {
			if (my $creator = $metaNode->textContent()) {
				$self->graph( { creator=>$creator } );
				print "inputGraphfromGEXF: set graph attribute creator: $creator\n" if $VERBOSE;
			}
		}
		
		if (my $metaNode = $xpc->findnodes('/gexf:gexf/gexf:meta/gexf:description')->[0] ) {
			if (my $label = $metaNode->textContent()) {
				$self->graph( { label=>$label } );
				print "inputGraphfromGEXF: set graph attribute label (from meta attribute description): $label\n" if $VERBOSE;
			}
		}
		
		
		foreach my $nodeElement ($xpc->findnodes('/gexf:gexf/gexf:graph/gexf:nodes/gexf:node')) {
			
			#my $node = $nodeElement->nodeName();
			my $id = $nodeElement->getAttribute('id');
			my $label = $nodeElement->getAttribute('label') || $EMPTY_STRING;
			$self->node( {id=>$id, label=>$label} );
			$nodecount++;
		}
		
		foreach my $edgeElement ($xpc->findnodes('/gexf:gexf/gexf:graph/gexf:edges/gexf:edge')) {
			
			#my $edge = $edgeElement->nodeName();
			my $source = $edgeElement->getAttribute('source') || $EMPTY_STRING;
			my $target = $edgeElement->getAttribute('target') || $EMPTY_STRING;
			my $weight = $edgeElement->getAttribute('weight') || $defaultWeight;
			if ($weight) {
				$self->edge( { sourceID=>$source, targetID=>$target, weight=>$weight } );
				$edgecount++;
			}
			else {
				carp "inputGraphfromGEXF: edge $source $target has no weight. Not created."
			}
		
		}
		
		carp "inputGraphfromGEXF: no nodes read from '$filename'" if !$nodecount;
		carp "inputGraphfromGEXF: no edges read from '$filename'" if !$edgecount;
		
		print "inputGraphfromGEXF: found $nodecount nodes and $edgecount edges\n" if $VERBOSE;
		
		return $self;
	}
	
	
	sub outputGraphtoGEXF {
		my ($self, $filename) = @_;
		
		my $nsURI = 'http://www.gexf.net/1.2draft';
		
		my $doc = XML::LibXML::Document->new('1.0','UTF-8');
		my $GEXF = $doc->createElementNS( $EMPTY_STRING, 'gexf' );
		$doc->setDocumentElement( $GEXF );
	 
		$GEXF->setNamespace( $nsURI , $EMPTY_STRING, 1 );
		
		$GEXF->setAttribute('xmlns:xsi','http://www.w3.org/2001/XMLSchema-instance');
		$GEXF->setAttribute('xsi:schemaLocation','http://www.gexf.net/1.2draft http://www.gexf.net/1.2draft/gexf.xsd');
		
		{
			my $now_string = localtime;
			$GEXF->appendChild($doc->createComment("Generated by Graph::Dijkstra on $now_string"));
		}
		{
			my (undef, undef, undef, $mday, $month, $year, undef, undef, undef) = localtime;
			my $ISODATE = sprintf "%4d-%02d-%02d", $year+1900, $month+1, $mday;
			my $meta = $GEXF->addNewChild( $nsURI, 'meta');
			$meta->setAttribute('lastmodifieddate', $ISODATE);
			if ($self->{creator}) {
				my $creatorNode = $meta->addNewChild( $nsURI, 'creator');
				$creatorNode->appendTextNode( $self->{creator} );
			}
			if ($self->{label}) {
				my $descriptionNode = $meta->addNewChild( $nsURI, 'description');
				$descriptionNode->appendTextNode( $self->{label} );
			}
		}
		
		my $graph = $GEXF->addNewChild( $nsURI, 'graph' );
		$graph->setAttribute('mode','static');
		$graph->setAttribute('defaultedgetype','undirected');
		my $nodesElement = $graph->addNewChild( $nsURI, 'nodes' );
		
		my $nodecount = 0;
		my $edgecount = 0;
		
		my %edges = ();
		foreach my $nodeID (keys %{$self->{graph}}) {
			
			my $nodeNode = $nodesElement->addNewChild( $nsURI, 'node' );
			$nodeNode->setAttribute('id', $nodeID);
			$nodeNode->setAttribute('label', $self->{graph}{$nodeID}{label} );
			
			$nodecount++;
			if (exists($self->{graph}{$nodeID}{edges})) {
				foreach my $targetID (keys %{$self->{graph}{$nodeID}{edges}}) {
					if (!exists($edges{$targetID}{$nodeID})) {
						$edges{$nodeID}{$targetID} = $self->{graph}{$nodeID}{edges}{$targetID};
					}
				}
			}
		}
		
		my $edgesElement = $graph->addNewChild( $nsURI, 'edges' );
		
		foreach my $sourceID (keys %edges) {
			foreach my $targetID (keys %{$edges{$sourceID}}) {
				
				$edgecount++;
				my $edgeNode = $edgesElement->addNewChild( $nsURI, 'edge');
				$edgeNode->setAttribute('id', $edgecount);
				$edgeNode->setAttribute('source', $sourceID );
				$edgeNode->setAttribute('target', $targetID );
				$edgeNode->setAttribute('weight', $edges{$sourceID}{$targetID} );
				
			}
		}
		my $state = $doc->toFile($filename,2);
		croak "could not output internal grap to '$filename'" if !$state;
	
		print "outputGraphtoGEXF: wrote $nodecount nodes and $edgecount edges to '$filename'\n" if $VERBOSE;
		return $self;
	}
	
} #XML file format methods

#############################################################################
#NET (Pajek) file format methods                                            #
#############################################################################
{
	sub inputGraphfromNET {
		my ($self, $filename) = @_;
		
		use Regexp::Common qw /delimited/;
		
		open(my $infile, '<:encoding(UTF-8)', $filename) or croak "inputGraphfromNET: could not open '$filename' for input";
		
		print "inputGraphfromNET: opened '$filename' for input\n" if $VERBOSE;
		
		my $nodes = 0;
		while (my $line = <$infile>) {
			if ($line =~ /^\*vertices\s+(\d+)/ix) {
				$nodes = $1;
				last;
			}
		}
		croak "inputGraphfromNET: vertices element not found" if !$nodes;
		print "inputGraphfromNET: vertices = $nodes\n" if $VERBOSE;
		
		my $nodecount = 0;
		my $edgecount = 0;
		my $quotedRE = $RE{delimited}{-delim=>'"'};
		#print "quotedRE = '$quotedRE'\n";
		
		my $nextSection = '';
		foreach my $i (1 .. $nodes) {
			my $line = '';
			while(1) {
				$line = <$infile>;
				chomp $line;
				croak "inputGraphfromNET: unexpected EOF in vertices section" if !defined($line);
				last if substr($line,0,1) ne '%';
			}
			
			if (substr($line,0,1) eq '*') {
				$nextSection = lc($line);
				last;
			}

			if ($line =~ /^\s*(\d+)\s+($quotedRE)/ix) {
				my $id = $1;
				my $label = $2;
				die "crap id not defined" if !defined($id);
				die "crap label not defined" if !defined($label);
				$label = substr($label,1,-1);  #strip quotes
				$self->node( {id=>$id, label=>$label} );
				$nodecount++;
			}
		}
		if ($nextSection and $nodecount == 0) {
			print "inputGraphfromNET: empty vertices section (no node labels).  Generating node ID values 1 .. $nodes\n" if $VERBOSE;
			foreach my $i (1 .. $nodes) {
				$self->node( {id=>$i, label=>$EMPTY_STRING} );
				$nodecount++;
			}
		}
		elsif ($nodes != $nodecount) {
			die "inputGraphfromNET: internal logic error: # vertices ($nodes) != # read nodes ($nodecount)";
		}
		
		croak "inputGraphfromNET: input file contains *arclist section.  Not supported." if substr($nextSection,0,8) eq '*arclist';
		croak "inputGraphfromNET: input file contains *edgelist section.  Not supported." if substr($nextSection,0,9) eq '*edgelist';
		carp  "inputGraphfromNET: input file contains *arcs section for directed graphs.  Not supported in this version." if substr($nextSection,0,5) eq '*arcs';
		
		my $foundEdgesSection = (substr($nextSection,0,6) eq '*edges') ? 1 : 0;
		
		if (!$foundEdgesSection) {
			while (my $line = <$infile>) {
				if (lc(substr($line,0,6)) eq '*edges') {
					$foundEdgesSection = 1;
					last;
				}
			}
		}
		croak "inputGraphfromNET: input file does not contain *edges section.  Not supported." if !$foundEdgesSection;
		
		while (my $line = <$infile>) {
			next if substr($line,0,1) eq '%';
			last if substr($line,0,1) eq '*';
			if ($line =~ /^\s+(\d+)\s+(\d+)\s+([0-9\.]+)/s) {
				$self->edge( { sourceID=>$1, targetID=>$2, weight=>$3 } );
				$edgecount++;
			}
			else {
				chomp $line;
				carp "inputGraphfromNET: unrecognized input line (maybe edge with no weight?) =>$line<=";
				last;
			}
		}
		close($infile);
		
		carp "inputGraphfromNET: no nodes read from '$filename'" if !$nodecount;
		carp "inputGraphfromNET: no edges read from '$filename'" if !$edgecount;
		
		print "inputGraphfromNET: found $nodecount nodes and $edgecount edges\n" if $VERBOSE;
		
		return $self;
	}
		
	sub outputGraphtoNET {
		my ($self, $filename) = @_;
		
		open(my $outfile, '>:encoding(UTF-8)', $filename) or croak "outputGraphtoNET: could not open '$filename' for output";
		
		print "outputGraphtoNET: opened '$filename' for output\n" if $VERBOSE;
		
		my %edges = ();
		my $nodecount = 0;
		my $edgecount = 0;
		my $useConsecutiveNumericIDs = 1;
		my $hasNonBlankLabels = 0;
		my $highestNumericID = 0;
		my $lowestNumericID = $PINF;
		
		my @nodeList = $self->nodeList();
		
		foreach my $nodeHref (@nodeList) {
			$nodecount++;
			my $nodeID = $nodeHref->{id};
			my $label = $nodeHref->{label};
			if ($useConsecutiveNumericIDs) {
				if ($nodeID =~ /^\d+$/) {
					$highestNumericID = $nodeID if $nodeID > $highestNumericID;
					$lowestNumericID = $nodeID if $nodeID < $lowestNumericID;
				}
				else {
					$useConsecutiveNumericIDs = 0;
				}
			}
			
			$hasNonBlankLabels = 1 if (!$hasNonBlankLabels and $label ne $EMPTY_STRING);
		}
		print "outputGraphtoNET: internal graph has non-blank labels.\n" if $VERBOSE and $hasNonBlankLabels;
		
		if ($useConsecutiveNumericIDs) {
			if ($highestNumericID != $nodecount or $lowestNumericID != 1) {
				$useConsecutiveNumericIDs = 0;
			}
		}
		
		
		{
			my $now_string = localtime;
			print {$outfile} "% Generated by Graph::Dijkstra on $now_string\n";
		}
		
		print {$outfile} "*Vertices $nodecount\n";
		
		if ($useConsecutiveNumericIDs) {

			print "outputGraphtoNET: internal graph has consecutive numeric IDs.\n" if $VERBOSE;
			$nodecount = 0;
			foreach my $nodeHref (sort { $a->{id} <=> $b->{id} } @nodeList) {
				
				$nodecount++;
				
				my $nodeID = $nodeHref->{id};
				my $label = $nodeHref->{label};
				croak "outputGraphtoNET: node IDs are not consecutive numeric integers starting at 1" if ($nodeID != $nodecount);
				
				if ($hasNonBlankLabels) {
					printf {$outfile} "%7d \"%s\"\n", $nodeID, $label;
				}
				
				if (exists($self->{graph}{$nodeID}{edges})) {
					foreach my $targetID (keys %{$self->{graph}{$nodeID}{edges}}) {
						if (!exists($edges{$targetID}{$nodeID})) {
							$edges{$nodeID}{$targetID} = $self->{graph}{$nodeID}{edges}{$targetID};
						}
					}
				}
			}
		}
		else {
			if ($VERBOSE) {
				print "outputGraphtoNET: internal graph node ID values are not consecutive integer values starting at 1.\n";
				print "outputGraphtoNET: internal graph node ID values not perserved in output\n";
				print "outputGraphtoNET: generating consecutive integer ID values in output\n";
			}
			
			my %nodeIDtoNumericID = ();
			foreach my $i (0 .. $#nodeList) {
				$nodeIDtoNumericID{ $nodeList[$i]->{id} } = $i+1;
			}
			
			foreach my $nodeID (sort {$nodeIDtoNumericID{$a} <=> $nodeIDtoNumericID{$b}} keys %nodeIDtoNumericID) {
				if ($hasNonBlankLabels) {
					printf {$outfile} "%7d \"%s\"\n", $nodeIDtoNumericID{$nodeID}, $self->{graph}{$nodeID}{label};
				}
				
				if (exists($self->{graph}{$nodeID}{edges})) {
					my $numericNodeID = $nodeIDtoNumericID{$nodeID};
					foreach my $targetID (keys %{$self->{graph}{$nodeID}{edges}}) {
						my $numericTargetID = $nodeIDtoNumericID{$targetID};
						if (!exists($edges{$numericTargetID}{$numericNodeID})) {
							$edges{$numericNodeID}{$numericTargetID} = $self->{graph}{$nodeID}{edges}{$targetID};
						}
					}
				}
			}
		}
		
		print {$outfile} "*Edges\n";
		foreach my $sourceID (sort {$a <=> $b} keys %edges) {
			foreach my $targetID (sort {$a <=> $b} keys %{$edges{$sourceID}} ) {
				printf {$outfile} "%7d %7d %10s\n", $sourceID, $targetID, "$edges{$sourceID}{$targetID}";
				$edgecount++;
			}
		}
		close($outfile);
		
		print "outputGraphtoNET: wrote $nodecount nodes and $edgecount edges to '$filename'\n" if $VERBOSE;
		return $self;
	}
	
	
}  #NET (Pagek) file format methods

1;

__END__


=head1 NAME
 
Graph::Dijkstra - Dijkstra's shortest path algorithm with methods to input/output graph datasets from/to supported file formats
 
=for comment '

=head1 SYNOPSIS
 
  # create the object
  use Graph::Dijkstra;
  my $graph = Graph::Dijkstra->new();
  my $graph = Graph::Dijkstra->new( {label=>'my graph label', creator=>'my name'} );  #include attributes hash to set the label and/or creator graph attibutes
  
  #SET method to update graph attributes
  $graph->graph( {label=>'my graph label', creator=>'my name'} );
  
  #GET method to return graph attributes in hash (reference)(eg., $graphAttribs->{label}, $graphAttribs->{creator})
  my $graphAttribs = $graph->graph();
  my $graphLabel = $graph->graph()->{label}; #references label attribute of graph
  
  #SET methods to create, update, and delete (remove) nodes and edges
  $graph->node( {id=>0, label=>'nodeA'} );   #create or update existing node.  id must be a simple scalar.
  $graph->node( {id=>1, label=>'nodeB'} );   #label is optional and should be string
  $graph->node( {id=>2, label=>'nodeC'} );
  $graph->edge( {sourceID=>0, targetID=>1, weight=>3} );  #create or update edge between sourceID and targetID;  weight (cost) must be > 0
  $graph->edge( {sourceID=>1, targetID=>2, weight=>2} );
  $graph->removeNode( 0 );
  $graph->removeEdge( {sourceID=>0, targetID=1} );
  
  #GET methods for nodes and edges
  my $nodeAttribs = $graph->node( 0 );  #returns hash reference (eg., $nodeAttribs->{id}, $nodeAttribs->{label}) or undef if node id 0 not found
  my $nodeLabel = $graph->node( 0 )->{label}; #references label attribute of node with ID value of 0
  my $bool = $graph->nodeExists( 0 );
  my $edgeAttribs = $graph->edge( { sourceID=>0, targetID=>1} );
  my $edgeWeight = $graph->edge( { sourceID=>0, targetID=>1} )->{weight};  #references weight attribute of edge that connects sourceID to targetID
  my $bool = $graph->edgeExists( { sourceID=>0, targetID=>1 } );
  my @nodes = $graph->nodeList();  #returns array of all nodes in the internal graph, each array element is a hash (reference) containing the node ID and label attributes
  my $bool = $graph->adjacent( { sourceID=>0, targetID=>1 } );
  my @nodes = $graph->adjacentNodes( 0 ); #returns list of node IDs connected by an edge with node ID 0
  
  #methods to input graph from a supported file format
  $graph->inputGraphfromGML('mygraphfile.gml');
  $graph->inputGraphfromCSV('mygraphfile.csv');
  $graph->inputGraphfromJSON('mygraphfile.json');   #JSON Graph Specification
  $graph->inputGraphfromGraphML('mygraphfile.graphml.xml', {keyEdgeValueID => 'weight', keyNodeLabelID => 'name'} );
  $graph->inputGraphfromGEXF('mygraphfile.gexf.xml' );
  $graph->inputGraphfromNET('mygraphfile.pajek.net' );   #NET (Pajek) format
  
  #methods to output graph to a supported file format
  $graph->outputGraphtoGML('mygraphfile.gml', 'creator name');
  $graph->outputGraphtoCSV('mygraphfile.csv');
  $graph->outputGraphtoJSON('mygraphfile.json');
  $graph->outputGraphtoGraphML('mygraphfile.graphml.xml', {keyEdgeWeightID => 'weight',keyEdgeWeightAttrName => 'weight', keyNodeLabelID => 'name', keyNodeLabelID => 'name'});
  $graph->outputGraphtoGEXF('mygraphfile.gexf.xml');
  $graph->outputGraphtoNET('mygraphfile.pajek.net');
  
  #Dijkstra shortest path computation methods
  
  use Data::Dumper;
  my %Solution = ();
  
  #shortest path to farthest node from origin node
  %Solution = (originID => 0);
  if (my $solutionWeight = $graph->farthestNode( \%Solution )) {
  	print Dumper(\%Solution);
  }
  
  #shortest path between two nodes (from origin to destination)
  %Solution = (originID => 0, destinationID => 2);
  if (my $pathCost = $graph->shortestPath( \%Solution ) ) {
  	print Dumper(\%Solution);
  }
  
  #Jordan or vertex center with all points shortest path matrix
  my %solutionMatrix = ();
  if (my $graphMinMax = $graph->vertexCenter(\%solutionMatrix) ) {
  	print "Center Node Set 'eccentricity', minimal greatest distance to all other nodes $graphMinMax\n";
  	print "Center Node Set = ", join(',', @{$solutionMatrix{centerNodeSet}} ), "\n";
  	
  	my @nodeList = (sort keys %{$solutionMatrix{row}});
  	print 'From/To,', join(',',@nodeList), "\n";
  	foreach my $fromNode (@nodeList) {
  		print "$fromNode";
  		foreach my $toNode (@nodeList) {
  			print ",$solutionMatrix{row}{$fromNode}{$toNode}";
  		}
  		print "\n";
  	}
  	$graph->outputAPSPmatrixtoCSV(\%solutionMatrix, 'APSP.csv');
  }
  
=head1 DESCRIPTION
 
Efficient implementation of Dijkstras shortest path algorithm in Perl using a Minimum Priority Queue (L<Array::Heap::ModifiablePriorityQueue>**).

Computation methods.

	farthestNode() Shortest path to farthest node from an origin node
	shortestPath() Shortest path between two nodes
	vertexCenter() Jordan center node set (vertex center) with all points shortest path (APSP) matrix

Methods that input and output graph datasets from/to the following file formats.

	GML (Graph Modelling Language, not to be confused with Geospatial Markup Language), 
	JSON Graph Specification (latest draft specification, edge weights/costs input using metadata "value" attribute), 
	GraphML (XML based), 
	GEXF (Graph Exchange XML Format), 
	NET (Pajek), and
	CSV (a simple row column format modelled after GML developed by author).

At this time, Graph::Dijkstra supports undirected graphs only.  All edges are treated as bi-directional (undirected).
The inputGraphfrom[file format] methods croak if they encounter a graph dataset with directed edges.

In this pre-production release, the internal graph data model is minimal. 
	The graph contains two (graph level) attributes: label (or description) and creator.
	Nodes (vertices) contain an ID value (simple scalar), a label (string) and a list (hash) of edges (the ID values of connected nodes).  
	Edges contain the ID values of the target and source nodes and the numeric weight (cost/value/distance/amount).  The edge weight must be a positive number (integer or real).

The outputGraphto[file format] methods output data elements from the internal graph.  If converting between two supported formats (eg., GML and GraphML), unsupported
attributes from the input file (which are not saved in the internal graph) are *not* be written to the output file.  Later releases will extend the internal graph data model.

This pre-production release has not been sufficiently tested with real-world graph data sets.  It can handle rather large datasets (tens of 
thousands of nodes, hundreds of thousands of edges).

If you encounter a problem or have suggested improvements, please email the author and include a sample dataset.
If providing a sample data set, please scrub it of any sensitive or confidential data.

**Array::Heap::ModifiablePriorityQueue, written in Perl, uses Array::Heap, an xs module.

=head1 METHODS
 
=head2 Class Methods
 
=over 4

=item Graph::Dijkstra->VERBOSE( $bool );

Class method that turns on or off informational output to STDOUT.

=item my $graph = Graph::Dijsktra->new();
 
Create a new, empty graph object. Returns the object on success.

=item my $graph = Graph::Dijsktra->new( {label=>'my graph label', creator=>'my name'} );
 
Create a new, empty graph object setting the two graph level attributes label and creator. Returns the object on success.

=back

=head2 Graph methods

=over 4

=item $graph->graph( {label=>'my graph label', creator=>'my name'} );

SET method that updates the graph level attributes label and creator.

=item my $graphAttribs = $graph->graph();

GET method that returns the graph level attributes label and creator in a hash reference (eg., C<< $graphAttribs->{label} >> and C<< $graphAttribs->{creator} >> );

=back

=head2 Node methods

=over 4

=item $graph->node( {id=>$id, label=>$label} );

SET method: creates or updates existing node and returns self.  Node id values must be simple scalars.

=item my $nodeAttribs = $graph->node( $id );
 
GET method: returns the hash (reference) with the id and label values for that node or undef if the node ID does not exist.  
Note: nodes may have blank ('') labels.  Use nodeExists method to test for existance.
 
=item my $bool = $graph->nodeExists( $id );
 
GET method: returns true if a node with that ID values exists or false if not.

=item my @list = $graph->nodeList();

Returns unsorted array of all nodes in the graph.  Each list element is a hash (reference) that contains an "id" and "label" attribute.
$list[0]->{id} is the id value of the first node and $list[0]->{label} is the label value of the first node.

=item $graph->removeNode( $id );
 
Removes node identified by $id and all connecting edges and returns self.  Returns undef if $id does not exist.
 			
=back

=head2 Edge methods

=over 4

=item $graph->edge( {sourceID=>$sourceID, targetID=>$targetID, weight=>$weight} );
 
SET method: creates or updates existing edge between $sourceID and $targetID and returns $self. $weight must be > 0. 
Returns undef if $weight <= 0 or if $sourceID or $targetID do not exist.
									 
=item my $graphAtrribs = $graph->edge( {sourceID=>$sourceID, targetID=>$targetID} );

GET method: returns hash reference with three attributes: sourceID, targetID, and weight (cost/distance/value of edge between sourceID and targetID).  
"weight" is 0 if there is no edge between sourceID and targetID (and sourceID and targetID both exist).
Returns undef if sourceID or targetID do not exist. 											 
 
=item my $bool = $graph->edgeExists( { sourceID=>$sourceID, targetID=>$targetID } );
 
GET method: returns true if an edge connects the source and target IDs or false if an edge has not been defined.
 
=item my $bool = $graph->adjacent( { sourceID=>$sourceID, targetID=>$targetID } );
 
GET method: returns true if an edge connects $sourceID and $targetID or false if not.  Returns undef if $sourceID or $targetID do not exist.

=item my @list = $graph->adjacentNodes( $id );
 
Returns unsorted list of node IDs connected to node $id by an edge.  Returns undef if $id does not exist.
 
=item $graph->removeEdge( { sourceID=>$sourceID, targetID=>$targetID } );
 
Removes edge between $sourceID and $targetID (and $targetID and $sourceID) and returns self.  Returns undef if $sourceID or $targetID do not exist.
	
=back

=head2 Dijkstra computation methods

=over 4

=item my $solutionWeight = $graph->farthestNode( $solutionHref );
 
Returns the cost of the shortest path to the farthest node from the origin.  Attribute originID must be set in the parameter (hash reference) $solutionHref.
Carps and returns 0 if originID attribute not set or if node ID value not found in internal graph.
Populates $solutionHref (hash reference) with total weight (cost) of the farthest node(s) from the originID and the edge list from the originID
to the farthest node(s).  When there is more than one solution (two or more farthest nodes from the origin with the same total weight/cost), the solution hash
includes multiple "path" elements, one for each farthest node.

Sample code

	my %Solution = (originID=>'I');
	if ( my $solutionWeight = $graph->farthestNode(\%Solution) ) {
		print Dumper(\%Solution);
		foreach my $i (1 .. $Solution{count}) {
			print "From originID $Solution{originID}, solution path ($i) to farthest node $Solution{path}{$i}{destinationID} at weight (cost) $Solution{weight}\n";
			foreach my $edgeHref (@{$Solution{path}{$i}{edges}}) {
				print "\tsourceID='$edgeHref->{sourceID}' targetID='$edgeHref->{targetID}' weight='$edgeHref->{weight}'\n";
			}
		}
	}

Produces the following output

	$VAR1 = {
          'weight' => 18,
          'originID' => 'I',
          'desc' => 'farthest',
          'count' => 2,
          'path' => {
                      '1' => {
                               'destinationID' => 'A',
                               'edges' => [
                                            {
                                              'sourceID' => 'I',
                                              'targetID' => 'L',
                                              'weight' => 4
                                            },
                                            {
                                              'weight' => 6,
                                              'targetID' => 'H',
                                              'sourceID' => 'L'
                                            },
                                            {
                                              'sourceID' => 'H',
                                              'targetID' => 'D',
                                              'weight' => 5
                                            },
                                            {
                                              'weight' => 3,
                                              'targetID' => 'A',
                                              'sourceID' => 'D'
                                            }
                                          ]
                             },
                      '2' => {
                               'destinationID' => 'C',
                               'edges' => [
                                            {
                                              'sourceID' => 'I',
                                              'targetID' => 'J',
                                              'weight' => 2
                                            },
                                            {
                                              'weight' => 9,
                                              'targetID' => 'K',
                                              'sourceID' => 'J'
                                            },
                                            {
                                              'targetID' => 'G',
                                              'sourceID' => 'K',
                                              'weight' => 2
                                            },
                                            {
                                              'weight' => 5,
                                              'sourceID' => 'G',
                                              'targetID' => 'C'
                                            }
                                          ]
                             }
                    }
        };

	From originID I, solution path (1) to farthest node A at weight (cost) 18
		sourceID='I' targetID='L' weight='4'
		sourceID='L' targetID='H' weight='6'
		sourceID='H' targetID='D' weight='5'
		sourceID='D' targetID='A' weight='3'
	From originID I, solution path (2) to farthest node C at weight (cost) 18
		sourceID='I' targetID='J' weight='2'
		sourceID='J' targetID='K' weight='9'
		sourceID='K' targetID='G' weight='2'
		sourceID='G' targetID='C' weight='5'


=item my $solutionWeight = $graph->shortestPath( $solutionHref );
 
Returns weight (cost) of shortest path between originID and destinationID (set in $solutionHref hash reference) or 0 if there is no path between originID and destinationID.
Carps if originID or destinationID not set or node ID values not found in internal graph.
Populates $solutionHref (hash reference) with total path weight (cost) and shortest path edge list.

Sample code

	my %Solution = ( originID=>'I', destinationID=>'A' );
	if ( my $pathCost = $graph->shortestPath(\%Solution) ) {
		print Dumper(\%Solution);
		print "Solution path from originID $Solution{originID} to destinationID $Solution{destinationID} at weight (cost) $Solution{weight}\n";
		foreach my $edgeHref (@{$Solution{edges}}) {
			print "\tsourceID='$edgeHref->{sourceID}' targetID='$edgeHref->{targetID}' weight='$edgeHref->{weight}'\n";
		}
	}
	
Produces the following output

	$VAR1 = {
          'destinationID' => 'A',
          'weight' => 18,
          'desc' => 'path',
          'originID' => 'I',
          'edges' => [
                       {
                         'weight' => 4,
                         'sourceID' => 'I',
                         'targetID' => 'L'
                       },
                       {
                         'targetID' => 'H',
                         'sourceID' => 'L',
                         'weight' => 6
                       },
                       {
                         'targetID' => 'D',
                         'weight' => 5,
                         'sourceID' => 'H'
                       },
                       {
                         'weight' => 3,
                         'sourceID' => 'D',
                         'targetID' => 'A'
                       }
                     ]
        };
        
	Solution path from originID I to destinationID A at weight (cost) 18
		sourceID='I' targetID='L' weight='4'
		sourceID='L' targetID='H' weight='6'
		sourceID='H' targetID='D' weight='5'
		sourceID='D' targetID='A' weight='3'

=item my $graphMinMax = $graph->vertexCenter($solutionMatrixHref);

Returns the graph "eccentricity", the minimal greatest distance to all other nodes from the "center node" set or Jordan center.
Carps if graph contains disconnected nodes (nodes with no edges) which are excluded.  If graph contains a disconnected sub-graph (a set of connected
nodes isoluated / disconnected from all other nodes), the return value is 0 -- as the center nodes are undefined.

The $solutionMatrix hash (reference) is updated to include the center node set (the list of nodes with the minimal greatest distance
to all other nodes) and the all points shortest path matrix.  In the all points shortest path matrix, an infinite value (1.#INF) indicates that there
is no path from the origin to the destination node.  In this case, the center node set is empty and the return value is 0.

Includes a class "helper" method that outputs the All Points Shortest Path matrix to a CSV file.

NOTE: The size of the All Points Shortest Path matrix is nodes^2 (expontial).  A graph with a thousand nodes results in a million entry matrix that
will take a long time to compute.  Have not evaluated the practical limit on the number of nodes.

Sample code

	my %solutionMatrix = ();
	
	my $graphMinMax = $graph->vertexCenter(\%solutionMatrix);
	print "Center Node Set = ", join(',', @{$solutionMatrix{centerNodeSet}} ), "\n";
	print "Center Node Set 'eccentricity', minimal greatest distance to all other nodes $graphMinMax\n";
	
	my @nodeList = (sort keys %{$solutionMatrix{row}});
	#  or (sort {$a <=> $b} keys %{$solutionMatrix{row}}) if the $nodeID values are numeric
	
	print 'From/To,', join(',',@nodeList), "\n";
	foreach my $fromNode (@nodeList) {
		print "$fromNode";
		foreach my $toNode (@nodeList) {
			print ",$solutionMatrix{row}{$fromNode}{$toNode}";
		}
		print "\n";
	}
	
	# Output All Points Shortest Path matrix to a .CSV file.  
	# If the nodeID values are numeric, include a third parameter, 'numeric' to sort the nodeID values numerically.
	
	$graph->outputAPSPmatrixtoCSV(\%solutionMatrix, 'APSP.csv');

=back

=head2 Input graph methods

=over 4

=item $graph->inputGraphfromJSON($filename);

Inputs nodes and edges from a JSON format file following the draft JSON Graph Specification. Only supports single graph files.  JSON Graph Specification files using
the "Graphs" (multi-graph) attribute are not supported.

If the Graph metadata section includes the "creator" attribute, sets the internal graph attribute "creator".

Edge values/weights/costs are input using the edge metadata "value" attribute.
Example edge that includes metadata value attribute per JSON Graph Specification.
	{
    "source" : "1",
    "target" : "2",
    "metadata" : {
     "value" : "0.5"
    }
  },

See JSON Graph Specification L<https://github.com/jsongraph/json-graph-specification>

=item $graph->inputGraphfromGML($filename);

Inputs nodes and edges from a Graphics Modelling Language format file (not to be confused with the Geospatial Markup Language XML format).  
Implemented using pattern matching (regexp's) on "node" and "edge" constructs.
An unmatched closing bracket (']') inside a quoted string attribute value will break the pattern matching.  
Quoted string attribute values (e.g., a label value) should not normally include an unmatched closing bracket.
Report as a bug and I'll work on re-implementing using a parser.

See Graph Modelling Language L<https://en.wikipedia.org/wiki/Graph_Modelling_Language>

=item $graph->inputGraphfromCSV($filename);

Inputs nodes and edges from a CSV format file loosely modelled after GML.  
The first column in each "row" is either a "node" or "edge" value.  For "node" rows, the next two columns are the ID and LABEL values.
For "edge" rows, the next three columns are "source", "target", and "value" values.  No column header row.

Example

	node,A,"one"
	node,B,"two"
	node,C,"three"
	node,D,"four"
	node,E,"five"
	node,F,"six"
	edge,A,B,4
	edge,A,F,5
	edge,A,E,7
	edge,A,D,3
	edge,D,E,5
	

=item $graph->inputGraphfromGraphML($filename, {keyEdgeValueID => 'weight', keyNodeLabelID => 'name'} );

Inputs nodes and edges from an XML format file following the GraphML specification.  EXPERIMENTAL... has not been tested with real world data sets.

Input files must contain only a single graph and cannot contain embedded graphs.  Hyperedges are not supported.

The options hash reference (second parameter following the filename) is used to provide the key element ID values for edge weight/value/cost/distance and node label/name/description.

If either is not provided, the method will search the key elements for (1) edge attributes (for="edge") with an attr.name value of weight, value, cost, or distance; 
and (2) node attributes (for="node") with an attr.name value of label, name, description, or nlabel.

Graphs must contain a "key" attribute for edges that identifies the edge weight/value/cost/distance such as C<< for="edge" attrib.name="weight" >>.  
If this key element includes a child element that specifies a default value, that default value will be used to populate the weight (cost/value/distance) for each edge node
that does not include a weight/value/cost/distance data element.  Seems odd to specify a default edge weight but it will be accepted.

 
  <key id="d1" for="edge" attr.name="weight" attr.type="double">
    <default>2.2</default>
  </key>

	<edge id="7" source="1" target="2">
		<data key="weight">0.5</data>
	</edge>

Graphs should contain a "key" attribute for nodes that identifies the node label / name / description such as C<< for="node" attrib.name="name" >> or C<< for="node" attrib.name="label" >>.
These are used to populate the internal graph "label" value for each node.  If not included, the internal node labels will be empty strings.

	<key id="name" for="node" attr.name="name" attr.type="string"/>
	
	<node id="4">
		<data key="name">josh</data>
	</node>

See GraphML Primer L<http://graphml.graphdrawing.org/primer/graphml-primer.html> and
GraphML example L<http://gephi.org/users/supported-graph-formats/graphml-format/>


=item $graph->inputGraphfromGEXF( $filename );

Inputs nodes and edges from an XML format file following the GEXF draft 1.2 specification.  Will also accept draft 1.1 specification files.

Input files must contain only a single graph.  Hierarchial nodes are not supported.

Node elements are expected to contain a label element.  Edge elements are expected to contain a weight attribute.

Note: Author has seen a GEXF file where the edge weights were specified using a <attvalues><attvalue> element under each <edge> element (see following) 
where there was no corresponding <attributes><attribute> definition of weight.  This doesn't appear to be correct.  If it is, please email the author.

	<attvalues>
		<attvalue for="weight" value="1.0"></attvalue>
	</attvalues>


=item $graph->inputGraphfromNET( $filename );

Input nodes and edges from NET (Pajek) format files.   At this time, inputs nodes from the C<< *Vertices >> section and edges from the C<< *Edges >> section.  All other 
sections are ignored including: C<< *Arcs >> (directed edges), C<< *Arcslist >>, and C<< *Edgeslist >>.  

NET (Pajek) files with multiple (time based) Vertices and Edges sections are not supported.


=back

=head2 Output graph methods

=over 4

=item $graph->outputGraphtoGML($filename, $creator);

Using the internal graph, outputs a file in GML format.  Includes a "creator" entry on the first line of the file with a date and timestamp.
Note that non-numeric node IDs and label values are HTML encoded.

=item $graph->outputGraphtoJSON( $filename );

Using the internal graph, outputs a file following the JSON graph specification.  In the Graph metadata section, includes a comment attribute referencing this
module and the local time that the JSON document was created.  See the inputGraphfromJSON method for format details.


=item $graph->outputGraphtoCSV( $filename );

Using the internal graph, outputs a file in CSV format.  See the inputGraphfromCSV method for format details.

=item $graph->outputGraphtoGraphML($filename, {keyEdgeWeightID => '',keyEdgeWeightAttrName => '', keyNodeLabelID => '', keyNodeLabelID => ''} );

Using the internal graph, outputs a file in XML format following the GraphML specification (schema).
The option attributes keyEdgeWeightID and keyEdgeWeightAttrName both default to 'weight'.  keyNodeLabelID and keyNodeLabelID both default to 'name'.  Set these 
attributes values only if you need to output different values for these key attributes.

=item $graph->outputGraphtoGEXF( $filename );

Using the internal graph, outputs a file in XML format following the GEXF draft 1.2 specification (schema).

=item $graph->outputGraphtoNET( $filename );

Using the internal graph, outputs a file in NET (Pajek) format. For node IDs, the NET format uses consecutive (sequential) numeric (integer) values (1 .. # of nodes).  
If the internal graph uses sequential numeric IDs, these will be preserved in the output file.  Otherwise, the existing node IDs are mapped to
sequentially numbered IDs that are output.  This preserves the graph node and edge structure but necessarily looses the existing node ID values.


=back

=head1 CHANGE HISTORY

=head2 Version 0.3		

	o Initial development release (first release that indexed correctly on CPAN)

=head2 Version 0.4

	o Added input/output methods for Pajek (NET) format files
	o Lots of incompatible changes.
	o Changed references to edge attribute labels to consistently use: sourceID, targetID, and weight.  
	o In the farthestNode and shortestPath methods, changed origin and destination to originID and destinationID as the starting and endpoint node ID values.
	o Changed the node, edge, removeEdge, adjacent, and edgeExists methods to use hash references as parameters.  Get version of node method returns hash reference.
		> Thought is that using hash references as parameters will better support future addition of graph, node, and edge attributes.
	o Changed the farthestNode and shortestPath methods to input the node ID value(s) in the solution hash reference parameter as "originID" and "destinationID".
	o Changed the solution hash reference returned by the farthestNode, shortestPath methods to use sourceID, targetID, and weight as hash attributes replacing source, target, and cost
	o Added two graph level attributes: label and creator.  Attributes are input / output from / to files as supported by that format.
	o Updated new method to accept an optional parameter hash (reference) with graph attributes
	o Added graph method to update (set) or return (get) graph attributes.
	o In files produced by the outputGraphto[file format] methods (exlcuding CSV files), added a comment "Generated by Graph::Dijkstra on [localtime string]".  In JSON, comment is a "metadata" attribute of the Graph object.
	o Validated that JSON text created by outputGraphtoJSON conforms to the JSON Graph Specification schema. 
	o Always bug fixes.
	o Updated test scripts.
	
=head2 Version 0.41

	o Updated edge (GET) method to return a hash reference with three attributes: sourceID, targetID, and weight.

=head1 PLATFORM DEPENDENCIES

Critical module depencies are XML::LibXML and Array::Heap::ModifiablePriorityQueue which itself uses Array::Heap.  XML::LibXML and Array::Head are XS modules.
XML::LibXML requires the c library libxml2.

For use with very large XML based graph files (GEXF or GraphML), recommend 64bit versions of Perl and the libxml2 (c) library.  See the "Performance" section.


=head1 PERFORMANCE

Performance measurements were recorded on an unremarkable laptop (Intel Core i5) running Microsoft Windows 10 (64bit) and ActiveState Perl 5.20.2 (x86). 
Reported times are Perl "use benchmark" measurements of the runtime of the core algorithm within the referenced methods.  Timings exclude data loading.
Measurements are indicative at best.

With a test graph of 16+K nodes and 121+K edges, both farthest and shortestPath completed in under 1 second (~0.45seconds).  Did not attempt to run
the vertexCenter method given that the resulting All Points Shortest Path matrix would have over 256M elements (requiring the computation of ~128M
unique paths).

With a smaller test graph of 809 nodes and 1081 edges, both farthestNode and shortestPath completed in under 0.01 seconds.
The vertexCenter method took much longer to complete at 56.61 seconds.  For a graph with 809 nodes, creating the All Points Shortest Path matrix 
required computation of 326,836 unique paths ((809^2 - 809) / 2).  The shortest path computation rate was ~5,774 paths per second or 
0.000173 seconds per path.   Next version will include an alternative implementation, the Floyd Warshall algorithm, to create the 
All Points Shortest Path matrix.

For the smaller test graph run, Windows Task Manager reported that Perl consumed 30-33% of CPU capacity and allocated 58.5MB of memory.

With a GraphML (XML) dataset containing over 700K nodes and 1M edges (>6M lines of text, ~175MB file size), the perl process ran out of memory 
(exceeded the 2GB per process limit for 32bit applications under 64bit MS Windows).  The memory allocation limit was reached in the libxml2 (c) library 
before control was returned to Perl.  Using the 64bit version of Perl should (may) avoid this problem.   The GraphML file is available at
L<http://sonetlab.fbk.eu/data/social_networks_of_wikipedia/> under the heading "Large Wikipedias (2 networks extracted automatically with the 2 algorithms)". 
Filename is eswiki-20110203-pages-meta-current.graphml.

 
=head1 LIMITATIONS / KNOWN ISSUES
 
Node ID values must be simple scalars.

Currently only works with undirected graphs (bidirectional edges) with explicit edge weights.  Production release (or earlier) will add support for directed graphs (unidirectional edges).

For speed and simplicity, reading GML format files (method inputGraphfromGML) is implemented using pattern matching (regexp's).  An unmatched closing bracket (']') inside a quoted string (value) will break it. 
For testing, implemented an alternative using Parse::Recdescent (recursive descent) that eliminated the "unmatched closing bracket insided a quoted string" problem.
Unfortunately, performance was unacceptably slow (really bad) on large graph files (10K+ nodes, 100K+ thousand edges).  Will continue to evaluate alternatives.

=head1 TODOs

Add data attributes for graphes, nodes and edges in a comprehensive manner.  Update $graph->inputGraphfrom*, $graph->outputGraphto*, $graph->node*, and $graph->edge* methods.  
Possible new attributes include:

	edge IDs and labels,
	graph edge default: directed / undirected,
	node graph coordinates (xy coordinates and/or lat/long),
	node and edge style (eg., line style, color)

Support input, Dijkstra path computations, and output of directed graphs (graphs with directed / uni-directional edges).

Evaluate vertexCenter method with larger graphs.  Current implementation uses Dijkstra shortest path algorithm. 
Possibly replace with Floyd Warshall algorithm or add second implementation.

Test very large graph datasets using a 64bit version of perl (without the 2GB process limit).

Rewrite the input/output methods for CSV files to process nodes and edges in separate files (nodes file and edges file) with each containing an appropriate column header.

Validate that GEXF and GraphML files output by the "outputGraphto[GEXF | GraphML]" methods conform to the associated XML schemas.

Input welcome. Please email author with suggestions.  Graph data sets for testing (purged of sensitive/confidential data) are welcome and appreciated.

 
=head1 SEE ALSO
 
L<Array::Heap::ModifiablePriorityQueue>

Graph Modelling Language L<https://en.wikipedia.org/wiki/Graph_Modelling_Language>

JSON Graph Specification L<https://github.com/jsongraph/json-graph-specification>

GraphML Primer L<http://graphml.graphdrawing.org/primer/graphml-primer.html>

GraphML example L<http://gephi.org/users/supported-graph-formats/graphml-format/>

GEXF file format L<http://www.gexf.net/format/index.html>

NET (Pajek) File format L<https://gephi.org/users/supported-graph-formats/pajek-net-format/>
 
=head1 AUTHOR
 
D. Dewey Allen C<< <ddallen16@gmail.com> >>

 
=head1 COPYRIGHT/LICENSE

Copyright (C) 2015, D. Dewey Allen

This program is free software; you can redistribute
it and/or modify it under the same terms as Perl itself. See L<perlartistic>.

=head1 DISCLAIMER OF WARRANTY
 
BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
NECESSARY SERVICING, REPAIR, OR CORRECTION.
 
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

=cut


