package Graph::Dijsktra;

use strict;
use warnings;

use Carp qw(croak carp);

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

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

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
	
## no critic (PostfixControls)

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


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


#############################################################################
#Methods                                                                    #
#############################################################################

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


sub _initialize {
	my $self = shift;
	$self->{graph} = ();
	#expect to add other code here
	return $self;
}

sub new {
	my $class = shift;
	
	my $self = {};
  bless $self, $class;

  return $self->_initialize();

  #return $self;
}

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

sub node {
	my ($self, $nodeA, $label) = @_;
	
	croak "node: missing nodeID parameter" if !defined($nodeA);
	
	if (defined($label)) {
		if (ref(\$nodeA) eq 'SCALAR') {
			$self->{graph}{$nodeA}{label} = $label;
			return $self;
		}
		croak "node: nodeID is not a SCALAR value";
	}
	
	if (exists($self->{graph}{$nodeA})) {
		return $self->{graph}{$nodeA}{label};
	}
	
	return;
}

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


sub nodeList {
	my $self = shift;
	
	my @nodeList = ();
	foreach my $node (keys %{$self->{graph}}) {
		push(@nodeList, [$node, $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, $nodeA, $nodeB, $cost) = @_;
	if (defined($cost)) {

		if ($cost <= 0) {
			carp "edge: invalid edge cost $nodeA $nodeB $cost";
			return;
		}

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


sub removeEdge {
	my ($self, $nodeA, $nodeB) = @_;
	
	if (!defined($nodeA) or !defined($nodeB)) {
		croak "removeEdge: missing nodeID parameters";
	}
		
	if (exists($self->{graph}{$nodeA}{edges}{$nodeB})) {
		delete($self->{graph}{$nodeA}{edges}{$nodeB});
		delete($self->{graph}{$nodeB}{edges}{$nodeA});
		foreach my $node ($nodeA, $nodeB) {
			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, $nodeA, $nodeB) = @_;
	
	if (!defined($nodeA) or !defined($nodeB) ) {
		croak "edgeExists: missing nodeID parameters";
	}
	
	return (exists($self->{graph}{$nodeA}{edges}{$nodeB})) ? 1 : 0;
}


sub adjacent {
	my ($self, $nodeA, $nodeB) = @_;
	
	if (!defined($nodeA) or !defined($nodeB) ) {
		croak "adjacent: missing nodeID parameters";
	}
	
	return ( exists($self->{graph}{$nodeA}{edges}{$nodeB}) ) ? 1 : 0;
}


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



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

#sub setSolutionMatrix {
#	my ($solutionMatrix, $origin, $destination, $cost) = @_;
#	$solutionMatrix->{row}{$origin}{$destination} = $cost;
#	$solutionMatrix->{row}{$destination}{$origin} = $cost;
#}

#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 outputAPSPmatrixtoCSV {
	my ($either, $solutionMatrix, $filename, $labelSort) = @_;
	
	use Text::CSV_XS;
	$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;
	
}


sub farthestNode {  ## no critic (ProhibitExcessComplexity)
	my ($self, $origin, $solutionHref) = @_;
	
	if (!defined($origin)) {
		carp "farthestNode: missing originID parameter";
		return 0;
	}
	
	if (!exists($self->{graph}{$origin})) {
		carp "farthestNode: origin node not found: $origin";
		return 0;
	}
	elsif (!exists($self->{graph}{$origin}{edges})) {
		carp "farthestNode: origin node $origin 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}{cost} = $PINF;
			$solution{$node}{prevnode} = $EMPTY_STRING;
			$pq->add($node, $PINF);
		}
	}
		
	$solution{$origin}{cost} = 0;
	$pq->add($origin,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 $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);
			}
		}
	}
	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 $farthestcost = 0;
	foreach my $node (sort keys %solution) {
		if ($solution{$node}{cost} < $PINF and $solution{$node}{cost} > $farthestcost) {
			$farthestcost = $solution{$node}{cost};
			#$farthestnode = $node;
		}
	}
	
	my $solutioncnt = 0;
	%{$solutionHref} = (
		desc => 'farthest',
		origin => $origin,
		cost => $farthestcost,
	);
	
	foreach my $farthestnode (sort keys %solution) {
		if ($solution{$farthestnode}{cost} == $farthestcost) {
			
			$solutioncnt++;
			
			print "\nfarthestNode: (solution $solutioncnt) farthest node from origin $origin is $farthestnode at cost $farthestcost\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}{destination} = $farthestnode;
			$solutionHref->{path}{$solutioncnt}{edges} = [];
				
			foreach my $i (0 .. $nexttolast) {
				
				push(@{$solutionHref->{path}{$solutioncnt}{edges}}, {source => $path[$i], target => $path[$i+1], cost => $self->edge($path[$i],$path[$i+1]) } );
				
			}

#  Commented out mid-point calculation	
#			my $pathcost = 0;
#			my $halfcost = $farthestcost / 2;
#			$fromNode = '';
#			while ($pathcost < $halfcost) {
#				my $nextNode = shift @path;
#				if ($nextNode eq '') {
#					print "problem in mid-point.\n";
#					print "pathcost = $pathcost\n";
#					print "halfcost = $halfcost\n";
#					print "fromnode = $fromNode\n";
#					last;
#				}
#				if ($fromNode ne '') {
#					my $edgecost = $self->{graph}{$fromNode}{edges}{$nextNode};
#					if ($pathcost + $edgecost > $halfcost) {
#						#print "center from $origin to $farthestnode is between $fromNode and $nextNode\n";
#						#print "length from $origin to $fromNode is $pathcost\n";
#						#print "length from $fromNode to $nextNode is $edgecost\n";
#						last;
#					}
#					$pathcost += $edgecost;
#				}
#				$fromNode = $nextNode;
#			}
		}
	}

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

sub shortestPath { ## no critic (ProhibitExcessComplexity)
	my ($self, $origin, $destination, $solutionHref) = @_;
	
	if (!exists($self->{graph}{$origin})) {
		carp "shortestPath: origin node not found: $origin";
		return 0;
	}
	if (!exists($self->{graph}{$origin}{edges})) {
		carp "shortestPath: origin node $origin has no edges";
		return 0;
	}
	if (!exists($self->{graph}{$destination})) {
		carp "shortestPath: destination node not found: $origin";
		return 0;
	}
	if (!exists($self->{graph}{$destination}{edges})) {
		carp "shortestPath: destination node $destination 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}{cost} = $PINF;
			$solution{$node}{prevnode} = $EMPTY_STRING;
			$pq->add($node, $PINF);
		}
	}
		
	$solution{$origin}{cost} = 0;
	$pq->add($origin,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 $destination) {
			$foundSolution = 1 if $solution{$visitNode}{cost} < $PINF;
			last;
		}
		
		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);
			}
		}
	}
	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 $pathcost = 0;
  if ($foundSolution) {
	  $pathcost = $solution{$destination}{cost};
		print "shortestPath: pathcost = $pathcost\n" if $VERBOSE;
		
		my $solutioncnt = 0;
		%{$solutionHref} = (
			desc => 'path',
			origin => $origin,
			destination => $destination,
			cost => $pathcost,
		);
		
		my $fromNode = $solution{$destination}{prevnode};
		my @path = ( $destination, $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}}, {source => $path[$i], target => $path[$i+1], cost => $self->edge($path[$i],$path[$i+1]) } );
		}
	}
	return($pathcost);
}



#############################################################################
#inputGraphfrom[File Format] Methods                                        #
#############################################################################

sub inputGraphfromCSV {
	my ($self, $filename) = @_;
	
	use Text::CSV_XS;

	my $nodecount = 0;
	my $edgecount = 0;
	
	open(my $infile, '<:encoding(UTF-8)', $filename) or croak "could not open '$filename'";

	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($row->[1], $row->[2]);
			$nodecount++;
		}
		elsif (lc($row->[0]) eq 'edge') {
			$self->edge($row->[1], $row->[2], $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 inputGraphfromJSON {
	my ($self, $filename) = @_;
	
	use JSON;
	
	my $json_text = $EMPTY_STRING;
	open(my $infile, '<:encoding(UTF-8)', $filename) or croak "could not open '$filename'";
	
	while (my $line = <$infile>) {
		$json_text .= $line;
	}
	close($infile);

	my $graphHref = from_json( $json_text, {utf8 => 1} ) or croak "invalid json text";
	
	if (ref($graphHref) ne 'HASH' or !exists($graphHref->{graph}{edges}) ) {
		croak "invalid JSON text, not a JSON graph specification file, or graph has no edges.";
	}
	
	if (exists($graphHref->{graph}{directed}) and $graphHref->{graph}{directed} ) {
		croak "inputGraphfromJSON: graph attribute \"directed\" is true (directed).  Not supported.";
	}
	
	my $nodecount = 0;
	my $edgecount = 0;
	
	foreach my $nodeHref (@{$graphHref->{graph}{nodes}}) {
		$nodecount++;
		$self->node($nodeHref->{id}, $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($edgeHref->{source}, $edgeHref->{target}, $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 inputGraphfromGML { ## no critic (ProhibitExcessComplexity)
	my ($self, $filename) = @_;
	
	use Regexp::Common;
	
	my $buffer = $EMPTY_STRING;
	my $linecount = 0;
	open(my $infile, '<:encoding(UTF-8)', $filename) or croak "could not open '$filename'";
	print "inputGraphfromGML: opened '$filename'\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."
		}
	}
	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( $keyvals{source}, $keyvals{target}, $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 inputGraphfromGraphML { ## no critic (ProhibitExcessComplexity)
	my ($self, $filename, $options) = @_;
	
	use XML::LibXML;
	
	my $dom = XML::LibXML->load_xml(location => $filename);
	
	my $xpc = XML::LibXML::XPathContext->new($dom);
	$xpc->registerNs('gml', 'http://graphml.graphdrawing.org/xmlns');
	
	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';
	}
	
	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\"]";
	}
	
	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,$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($source,$target,$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;
}


#############################################################################
#outputGraphto[File Format] methods                                         #
#############################################################################


sub outputGraphtoGML {
	my ($self, $filename, $creator) = @_;
	
	use HTML::Entities qw(encode_entities);
	
	open(my $outfile, '>:encoding(UTF-8)', $filename) or croak "could not open '$filename'";
	
	print "outputGraphtoGML: opened '$filename' for output\n" if $VERBOSE;
	
	my $now_string = localtime;
	
	print {$outfile} "Creator \"$creator on $now_string\"\n";
	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;
}


sub outputGraphtoCSV {
	my ($self, $filename) = @_;
	
	use Text::CSV_XS;
	
	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 outputGraphtoJSON {
	my ($self, $filename) = @_;
	
	use JSON;
	
	my $nodecount = 0;
	my $edgecount = 0;
	
	my %graph = ();
	$graph{directed} = JSON::false;
	@{$graph{graph}{nodes}} = ();
	@{$graph{graph}{edges}} = ();
	
	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;
}

sub outputGraphtoGraphML {
	my ($self, $filename, $options) = @_;
	
	use XML::LibXML;
	
	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 );
	
	$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' );
	$keyNode->setAttribute('for','edge');
	$keyNode->setAttribute('id', $keyEdgeWeightID );
	$keyNode->setAttribute('attr.name', $keyEdgeWeightAttrName );
	$keyNode->setAttribute('attr.type', 'double' );
	
	my $graph = $graphML->addNewChild( $nsURI, 'graph' );
	$graph->setAttribute('id','G');
	$graph->setAttribute('edgedefault','undirected');
	
	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('id', $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('id', $keyEdgeWeightID );
			$dataNode->appendTextNode( $edges{$sourceID}{$targetID} );
			
		}
	}
	
	open(my $outfile, '>:encoding(UTF-8)', $filename) or croak "could not open '$filename' for output";
	
	print "outputGraphtoGraphML: opened '$filename' for output\n" if $VERBOSE;
	
	print {$outfile} $doc->toString(2);
	close($outfile);
	print "outputGraphtoGraphML: wrote $nodecount nodes and $edgecount edges to '$filename'\n" if $VERBOSE;
	return $self;
}

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();
 
  # input graph from a supported file format
  $graph->inputGraphfromGML('astro-ph.gml');
  $graph->inputGraphfromCSV('another.csv');
  $graph->inputGraphfromJSON('another.json');
  $graph->inputGraphfromGraphML('graphML.xml', {keyEdgeValueID => 'weight', keyNodeLabelID => 'name'} );
  
  #SET methods to create graph nodes and edges "manually"
  $graph->node( 0, 'nodeA');
  $graph->node( 1, 'nodeB');
  $graph->node( 2, 'nodeC');
  $graph->edge(0, 1, 3);  #create or update an edge between source and target;  cost(dist) must be > 0
  $graph->edge(1, 2, 2);
  $graph->removeNode( 0 ); #removes the node with ID = 0 and all associated edges; returns "self" or undef if node not found
  $graph->removeEdge( 0, 1 ); #removes the edge that connects the two nodes; returns "self" or undef if either node or the edge not found
  
  #GET methods for graph nodes and edges
  $graph->node( 0 ); #returns the label associated with node ID '0' or undef if there is no node with that id
  $graph->nodeExists( 0 );  #returns true if a node with an ID value of '0' has been defined; false if not.
  $graph->edge( 0, 1 ); #returns the cost (distance) associated with the edge between node's 0 and 1; undef if there is no edge
  $graph->edgeExists( 0, 1 );  #returns true if an edge between nodes with id values of '0' and '1' has been defined; false if not.
  $graph->nodeList();  #returns 2 dimensional array of all nodes, each node (array) element contains ID and LABEL values
  $graph->adjacent( 0, 1 ); #returns true or false if there is an edge between nodeA and nodeB
  $graph->adjacentNodes( 0 ); #returns a list of node IDs of immediately connected nodes (edges)
  
  #methods to output the internal graph to a supported file format
  $graph->outputGraphtoGML('mygraphfile.gml', 'creator name');
  $graph->outputGraphtoCSV('mygraphfile.csv');
  $graph->outputGraphtoJSON('mygraphfile.json');
  $graph->outputGraphtoGraphML('mygraphfile.xml', {keyEdgeWeightID => 'weight',keyEdgeWeightAttrName => 'weight', keyNodeLabelID => 'name', keyNodeLabelID => 'name'});
  
  #Dijkstra shortest path methods
  
  use Data::Dumper;
  my %Solution = ();
  
  #shortest path to farthest node from origin node
  if (my $solutionCost = $graph->farthestNode( 0, \%Solution )) {
  	print Dumper(\%Solution);
  }
  
  #shortest path between two nodes (from origin to destination)
  if (my $pathCost = $graph->shortestPath( 0, 2, \%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) C<$graph->farthestNode()>
	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 specification), 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 initial release, the internal graph data model is minimal.  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 cost (value/distance/amount).
The edge cost 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 initial 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 PLATFORM DEPENDENCIES

Graph::Dijkstra uses XML::LibXML to input and output GraphML format files.   At this time, XML::LibXML is not available on the ActiveState
PPM repository for Windows x86 or x64. Author has emailed ActiveState requesting that they make it available. Until such time as XML::LibXML
is available on the ActiveState PPM repository for Windows, this module will not build on the ActiveState PPM repository for Windows.  

Conversely, this module was developed using ActiveState Perl 5.20.2 for Windows (x86) using the XML::LibXML PPM installed from the Bribes PPM repository.
It should be possible for ActiveState Windows users to install Graph::Dijkstra as follows.

Install XML::LibXML from the Bribes PPM repository L<http://www.bribes.org/perl/ppmdir.html> using the following command.
	
		ppm install http://www.bribes.org/perl/ppm/XML-LibXML.ppd
	
Then install the following from the ActiveState PPM repository as normal (using the PPM install command).
	
		Array::Heap::ModifiablePriorityQueue
		Regexp::Common
		Scalar::Util
		HTML::Entities
	
You should then be able to install this module directly from CPAN using the CPAN command.

=head1 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.

=back

=head2 Node methods

=over 4

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

SET method that adds new or updates existing node and returns self.  Node ID values must be simple scalars.

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

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

Returns unsorted, 2 dimensional array (list of lists) of all nodes in the graph.  Each list element includes a node ID value (element 0) and LABEL value (element 1).
$list[0][0] is the ID value of the first node and $list[0][1] 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, $targetID, $cost );
 
SET method that creates new or updates existing edge between $sourceID and $targetID and returns $self. $cost must be > 0. 
Returns undef if $cost <= 0 or if $sourceID or $targetID do not exist.
									 
=item $graph->edge( $sourceID, $targetID );

GET method that returns existing cost (distance) of edge between $sourceID and $targetID.  Returns 0 if there is no edge between $sourceID and $targetID.
Returns undef if $sourceID or $targetID do not exist. 											 
 
=item $graph->edgeExists( $sourceID, $targetID );
 
GET method that returns true if an edge connects the source and target IDs or false if an edge has not been defined.
 
=item $graph->adjacent( $sourceID, $targetID );
 
GET method that 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, $targetID );
 
Removes edge between $source and $target (and $target and $source) and returns self.  Returns undef if $source or $target do not exist.
	
=back

=head2 Dijkstra computation methods

=over 4

=item my $solutioncost = $graph->farthestNode( $originID, $solutionHref );
 
Returns the cost of the shortest path to the farthest node from the origin.  Carps and returns 0 if $originID does not exist.
When there is more than one solution (two or more farthest nodes from the origin with the same cost), the solution hash
includes multiple "path" elements, one for each solution.

Sample code

	my %Solution = ();
	if ( my $solutionCost = $graph->farthestNode('I', \%Solution) ) {
		print Dumper(\%Solution);
		foreach my $i (1 .. $Solution{count}) {
			print "From origin $Solution{origin}, solution path ($i) to farthest node $Solution{path}{$i}{destination} at cost $Solution{cost}\n";
			foreach my $edgeHref (@{$Solution{path}{$i}{edges}}) {
				print "\tsource='$edgeHref->{source}' target='$edgeHref->{target}' cost='$edgeHref->{cost}'\n";
			}
		}
	}

Produces the following output

	$VAR1 = {
          'cost' => 18,
          'origin' => 'I',
          'desc' => 'farthest',
          'count' => 2,
          'path' => {
                      '1' => {
                               'destination' => 'A',
                               'edges' => [
                                            {
                                              'source' => 'I',
                                              'target' => 'L',
                                              'cost' => 4
                                            },
                                            {
                                              'cost' => 6,
                                              'target' => 'H',
                                              'source' => 'L'
                                            },
                                            {
                                              'source' => 'H',
                                              'target' => 'D',
                                              'cost' => 5
                                            },
                                            {
                                              'cost' => 3,
                                              'target' => 'A',
                                              'source' => 'D'
                                            }
                                          ]
                             },
                      '2' => {
                               'destination' => 'C',
                               'edges' => [
                                            {
                                              'source' => 'I',
                                              'target' => 'J',
                                              'cost' => 2
                                            },
                                            {
                                              'cost' => 9,
                                              'target' => 'K',
                                              'source' => 'J'
                                            },
                                            {
                                              'target' => 'G',
                                              'source' => 'K',
                                              'cost' => 2
                                            },
                                            {
                                              'cost' => 5,
                                              'source' => 'G',
                                              'target' => 'C'
                                            }
                                          ]
                             }
                    }
        };

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


=item my $solutioncost = $graph->shortestPath( $originID, $destinationID, $solutionHref );
 
Returns cost of shortest path between $originID and $destinationID or 0 if there is no path between $originID and $destinationID.  Carps if $originID or $destinationID do not exist.
Populates $solutionHref (hash reference) with origin, destination, cost, and shortest path edges.

Sample code

	my %Solution = ();
	if ( my $pathCost = $graph->shortestPath('I','A', \%Solution) ) {
		print Dumper(\%Solution);
		print "Solution path from origin $Solution{origin} to destination $Solution{destination} at cost $Solution{cost}\n";
		foreach my $edgeHref (@{$Solution{edges}}) {
			print "\tsource='$edgeHref->{source}' target='$edgeHref->{target}' cost='$edgeHref->{cost}'\n";
		}
	}
	
Produces the following output

	$VAR1 = {
          'destination' => 'A',
          'cost' => 18,
          'desc' => 'path',
          'origin' => 'I',
          'edges' => [
                       {
                         'cost' => 4,
                         'source' => 'I',
                         'target' => 'L'
                       },
                       {
                         'target' => 'H',
                         'source' => 'L',
                         'cost' => 6
                       },
                       {
                         'target' => 'D',
                         'cost' => 5,
                         'source' => 'H'
                       },
                       {
                         'cost' => 3,
                         'source' => 'D',
                         'target' => 'A'
                       }
                     ]
        };
        
	Solution path from origin I to destination A at cost 18
		source='I' target='L' cost='4'
		source='L' target='H' cost='6'
		source='H' target='D' cost='5'
		source='D' target='A' cost='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. Edge values/weights/costs are input
using the 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://www.npmjs.com/package/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/>

=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.  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.

=back

=head1 PERFORMANCE

On an unremarkable laptop, with a test graph of 16+K nodes and 121+K edges, both farthest and shortestPath complete in under 1 second.  
Reading files from disk takes longer than computing the shortest path to the farthest node.
 
=head1 LIMITATIONS
 
Node ID values must be simple scalars.

Currently only works with undirected graphs.

For simplicity, InputGraphfromGML implemented using pattern matching (regexp's).  An unmatched closing bracket inside a quoted string (value) will break it. 
Will re-implement using a parser as necessary.

=head1 TODOs

Support input and output of addditional data attributes for nodes and edges in a comprehensive manner.  For example, add support for edge IDs and labels.
Update $graph->inputGraphfrom*, $graph->outputGraphto*, $graph->node*, and $graph->edge* methods.

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

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

Support additional graph file formats.

Input welcome. Please email author with suggestions.  Graph data sets (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://www.npmjs.com/package/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/>
 
=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.

 
=cut


