
/*
 * PATHSTATS.C - Sample news articles in spool and generate a POSTSCRIPT graph
 *
 *	V1.00 Written by Matthew Dillon, 12 April 1996
 *	(c) Copyright 1996, Matthew Dillon - permission granted to freely
 *	redistribute and modify as long as all original authors remain 
 *	credited.
 *
 *	This program samples news articles in the news spool and generates a
 *	postscript graph.  The program attempts to present an uncluttered
 *	graph but this is pretty much impossible, so users of the program
 *	should play with the -v, -t, and -f options to obtain a reasonably
 *	looking graph.  A few examples follows:
 *
 *	More Complex graph:
 *	    cd /var/spool/news/comp/sys
 *	    pathstats -t 5 -v 3 . > /tmp/x.ps
 *
 *	Less Complex graph:
 *	    cd /var/spool/news/comp/sys
 *	    pathstats -t 5 -v 10 . > /tmp/x.ps
 *
 *	    cd /var/spool/news/comp/sys
 *	    pathstats -t 5 -f 3 . > /tmp/x.ps
 *
 *	-t #	The program should scan every Nth article, default 1
 *	-v #	The program should ignore nodes references less then N times
 *	-f #	The program should ignore nodes with fewer then N fan-in.
 *
 *	With a small sample, these options can be left alone.  With a large
 *	sample, use these options to reduce the complexity of the graph.
 *	You can also generate a small sample from a larger hierarchy using
 *	the -t option to take only every Nth article.
 *
 *	The -v option normally ranges from 1 to 3 and is independant of the
 *	sample size.  However, it is more likely to miss major nodes that
 *	have few feeds but act as conduits between major portions of the
 *	network.
 *
 *	The -f option normally ranges from 1 to 50 and is highly dependant
 *	on the sample size.
 *
 *  THEORY OF OPERATION
 *
 *	This program collects statistics from an article sample which it
 *	obtains by scanning the specified directory hierarchy. The Path:
 *	line is extracted from each article and two parameters recorded:
 *
 *	(1) The overall number of references to any given node.. the 'volume',
 *	(2) A graph is built up for each node pair (A,B) where A feeds B.
 *
 *	The resulting node list is sorted by FAN-IN, which gives some sense
 *	of the volume each node will take up on the graph.  However, the node
 *	with the highest VOLUME is picked out and always placed in the middle.
 *	This is normally your 'home' node, since all news must flow to it.
 *
 *	A non-looping path is generated for the graph from the leafs toward
 *	the center(s).  
 *
 *	A complexity field (ha_Radius) is generated for each node via another
 *	graph traversal.  This field will be used to assign area-usage
 *	criterea in graphing the nodes.
 *
 *	The highest 5 fan-in sites are placed in an outer circle and the next
 *	highest 5 fan-in sites are placed in an inner circle.  The radius of
 *	the two circles is based on the complexity of each node.  Remaining
 *	nodes are given angular assignments around their 'parent' based on 
 *	their complexity verses the total complexity of the parent.
 *
 *	A few exceptional cases are handled as special cases... for example,
 *	the case where a node is a leaf or is in a chain.
 *
 *	The thickness of each segment depicts the volume flowing through that
 *	segment and the size of the square 'dot' for each node depicts the
 *	volume flowing through the node.  Since we can only determine transit
 *	volume and since all articles wind up at our site, our site and nearby
 *	sites will have the thickest lines.
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/dir.h>
#include <sys/mman.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <math.h>

#define HSIZE	4096
#define HMASK	(HSIZE-1)

#define VERSION	"1.00"

#define MINRADIUS	100

#define swap(type,v1,v2) { type t1 = (v1); v1 = (v2); v2 = t1; }

typedef struct Hash {
    struct Hash *ha_Next;
    struct Hash *ha_Parent;
    char *ha_Name;
    int ha_Hv;
    int ha_NameLen;
    int ha_Count;
    int ha_PlotX;
    int ha_PlotY;
    short ha_DidPlot0;
    short ha_DidPlot1;
    short ha_DidPlot2;
    short ha_PreSet;
    int	ha_NumLinks;
    int ha_Radius;
    double ha_StartTheta;
} Hash;

typedef struct Transit {
    struct Transit *tr_Next1;	/* link by Hash1->ha_Hv */
    struct Transit *tr_Next2;	/* link by Hash2->ha_Hv */
    Hash	*tr_Hash1;
    Hash	*tr_Hash2;
    int		tr_Count;
} Transit;

Hash *HashAry[HSIZE];
Transit *TransitAry1[HSIZE];
Transit *TransitAry2[HSIZE];
int NumHash;
int NumTransit;
int TrMaxCount;
int HaMaxCount;
int PlotOpt = 1;	/* plot by default */
int SimplifyFan = 1;
int SimplifyVol = 1;
int GraphNodes;
int Take = 1;
int PlotMinX;
int PlotMaxX;
int PlotMinY;
int PlotMaxY;

void ScanDir(const char *path, int tab);
void ScanFile(const char *fileName, int size);
const char * FindHeader(const char *base, const char *head, int size, const char **pend);
Hash *AddNode(const char *field, int fieldLen);
void AddTransit(Hash *hash1, Hash *hash2);
void PrintStats(void);
void PlotStats(void);

int psQSortA(const void *h1, const void *h2);
int psQSortB(const void *h1, const void *h2);
void IRotate(int x, int y, double theta, int *px, int *py);
void IBoundingBox(Hash *hash, int x, int y);

int
main(int ac, char **av)
{
    int i;
    char *newsDir = NULL;

    for (i = 1; i < ac; ++i) {
	char *ptr = av[i];

	if (*ptr != '-') {
	    newsDir = ptr;
	    continue;
	}
	ptr += 2;
	switch(ptr[-1]) {
	case 't':
	    ptr = (*ptr) ? ptr : av[++i];
	    if ((Take = strtol(ptr, NULL, 0)) < 1)
		Take = 1;
	    break;
	case 'f':
	    ptr = (*ptr) ? ptr : av[++i];
	    if ((SimplifyFan = strtol(ptr, NULL, 0)) < 1)
		SimplifyFan = 1;
	    break;
	case 'v':
	    ptr = (*ptr) ? ptr : av[++i];
	    if ((SimplifyVol = strtol(ptr, NULL, 0)) < 1)
		SimplifyVol = 1;
	    break;
	case 's':
	    PlotOpt = 0;	/* summary only	*/
	    break;
	default:
	    i = ac;
	    break;
	}
    }
    if (newsDir == NULL || i > ac) {
	printf("V%s\n%s\n",
	    VERSION,
	    "pathstats [-f min-fan-in] [-v min-volume] [-t art-skip#] [-s] dir"
	);
	exit(0);
    }
    ScanDir(newsDir, 0);
    if (PlotOpt)
	PlotStats();
    else
	PrintStats();
    exit(0);
}

void
ScanDir(const char *path, int tab)
{
    fprintf(stderr, "%*.*s%s {\n", tab, tab, "", path);
    if (chdir(path) == 0) {
	DIR *dir;

	if ((dir = opendir(".")) != NULL) {
	    struct direct *den;
	    int take = 0;

	    while ((den = readdir(dir)) != NULL) {
		struct stat st;

		if (den->d_name[0] == '.')
		    continue;

		if (lstat(den->d_name, &st) == 0) {
		    if (S_ISDIR(st.st_mode)) {
			ScanDir(den->d_name, tab + 4);
		    } else if (S_ISREG(st.st_mode)) {
			if (++take < Take)
			    continue;
			take = 0;
			ScanFile(den->d_name, st.st_size);
		    }
		}
	    }
	    closedir(dir);
	}
	chdir("..");
    }
    fprintf(stderr, "%*.*s}\n", tab, tab, "");
}

void
ScanFile(const char *fileName, int size)
{
    int fd;
    const char *base;

    if (size > 0 && (fd = open(fileName, O_RDONLY)) >= 0) {
	base = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
	close(fd);
	if (base != NULL && base != (const char *)-1) {
	    const char *dataEnd;
	    const char *data = FindHeader(base, "Path:", size, &dataEnd);
	    Hash *hash = NULL;

	    while (data && data < dataEnd) {
		const char *next;
		
		if ((next = strchr(data, '!')) != NULL && next < dataEnd) {
		    Hash *nhash;

		    nhash = AddNode(data, next - data);
		    if (hash)
			AddTransit(hash, nhash);
		    hash = nhash;
		    ++next;
		}
		data = next;
	    }
	    munmap((void *)base, size + 1);
	}
    }
}

const char *
FindHeader(const char *base, const char *head, int size, const char **pend)
{
    int hlen = strlen(head);

    while (size > 0 && *base && *base != '\n') {
	const char *line = base;

	/*
	 * Locate end of header
	 */

	for (;;) {
	    while (line - base < size && *line != '\n')
		++line;
	    if (line - base >= size)
		break;
	    ++line;
	    if (line - base >= size)
		break;
	    if (*line == ' ' || *line == '\t')
		continue;
	    break;
	}

	if (hlen <= line - base && strncmp(base, head, hlen) == 0) {
	    base += hlen;
	    while (base < line && (*base == ' ' || *base == '\t'))
		++base;
	    *pend = line - 1;
	    return(base);
	}

	/*
	 * go on to next header
	 */
	size -= line - base;
	base = line;
    }
    return(NULL);
}

Hash *
AddNode(const char *field, int fieldLen)
{
    int hv = strhash(field, fieldLen);
    Hash **phash;
    Hash *hash;

    for (
	phash = &HashAry[hv & HMASK]; 
	(hash = *phash) != NULL;
	phash = &hash->ha_Next
    ) {
	if (hash->ha_Hv == hv && 
	    hash->ha_NameLen == fieldLen &&
	    strncmp(field, hash->ha_Name, fieldLen) == 0
	) {
	    break;
	}
    }
    if (hash == NULL) {
	hash = calloc(sizeof(Hash) + fieldLen + 1, 1);
	*phash = hash;
	hash->ha_Name = (char *)(hash + 1);
	bcopy(field, hash->ha_Name, fieldLen);
	hash->ha_Name[fieldLen] = 0;
	hash->ha_NameLen = fieldLen;
	hash->ha_Hv = hv;
	++NumHash;
    }
    if (++hash->ha_Count > HaMaxCount)
	HaMaxCount = hash->ha_Count;
    return(hash);
}

void
AddTransit(Hash *hash1, Hash *hash2)
{
    Transit **ptran;
    Transit *tran;

    /*
     * Lookup hash1 by hash1
     */
    
    for (
	ptran = &TransitAry1[hash1->ha_Hv & HMASK];
	(tran = *ptran) != NULL;
	ptran = &tran->tr_Next1
    ) {
	if (tran->tr_Hash1 == hash1 &&
	    tran->tr_Hash2 == hash2
	) {
	    break;
	}
    }
    if (tran == NULL) {
	tran = calloc(sizeof(Transit), 1);
	tran->tr_Hash1 = hash1;
	tran->tr_Hash2 = hash2;
	*ptran = tran;					/* link hash1 */

	++hash1->ha_NumLinks;

	ptran = &TransitAry2[hash2->ha_Hv & HMASK];	/* link hash2 */
	tran->tr_Next2 = *ptran;
	*ptran = tran;

	++NumTransit;
    }
    if (++tran->tr_Count > TrMaxCount)
	TrMaxCount = tran->tr_Count;
}

int
strhash(const char *str, int len)
{
    int hv = 0xA45BFD2C;

    while (len) {
	hv = (hv << 5) ^ *str ^ (hv >> 23);
	++str;
	--len;
    }
    return(hv ^ (hv >> 16));
}

/*
 * OUTPUT STAGE - PRINT
 */

void
PrintStats(void)
{
    int i;

    for (i = 0; i < HSIZE; ++i) {
	Hash *hash;

	for (hash = HashAry[i]; hash; hash = hash->ha_Next) {
	    printf("NODE %d\t%s\n", hash->ha_Count, hash->ha_Name);
	}
    }

    for (i = 0; i < HSIZE; ++i) {
	Transit *tran;

	for (tran = TransitAry1[i]; tran; tran = tran->tr_Next1) {
	    printf("TRAN %d\t%s!%s\n", 
		tran->tr_Count,
		tran->tr_Hash1->ha_Name,
		tran->tr_Hash2->ha_Name
	    );
	}
    }
}

/*
 * OUTPUT STAGE - PLOT
 *
 * Plot concentric circles of 5 stages each based on ha_Count,
 * then draw line segments based on tr_Count
 */

void
PlotStats(void)
{
    Hash **ary;
    int i;
    int n;
    void psPlot0(Hash *hash);
    void psPlot1(Hash *hash);
    void psPlot2(Hash *hash);
    void psPlot3(Hash *hash);

    /*
     * vectorize Hash
     */
    
    ary = malloc(sizeof(Hash *) * NumHash);
    for (i = n = 0; i < HSIZE; ++i) {
	Hash *hash;

	for (hash = HashAry[i]; hash; hash = hash->ha_Next) {
	    ary[n++] = hash;
	}
    }

    /*
     * ary[0] is us - make it the ultimate root from a generational
     * standpoint even if it does not have the highest fan-in.
     */

    if (n)
	qsort(ary, n, sizeof(Hash *), psQSortA);	/* BY VOLUME	*/

    if (n) {
	Hash *hash = ary[0];
	hash->ha_PlotX = 1;
	hash->ha_PlotY = 1;
    }

    /*
     * Construct block from the leaf inward to the root based on fan-in.
     * Also get a sense of the graph's complexity.
     */

    if (n > 1) 
	qsort(ary + 1, n - 1, sizeof(Hash *), psQSortB); /* FANIN SORT */

    for (i = n - 1; i >= 0; --i) {
	Hash *hash = ary[i];
	Transit *tran = TransitAry2[hash->ha_Hv & HMASK];

	if (hash->ha_NumLinks < SimplifyFan || hash->ha_Count < SimplifyVol)
	    continue;

	++GraphNodes;

	while (tran) {
	    if (hash == tran->tr_Hash2) {
		Hash *h1 = tran->tr_Hash1;

		if (hash->ha_Parent == NULL ||
		    hash->ha_Parent->ha_NumLinks < h1->ha_NumLinks
		) {
		    hash->ha_Parent = h1;
		}
	    }
	    tran = tran->tr_Next2;
	}
    }

    /*
     * The leaf-inward construction might result in isolated subgraphs.
     *
     * Locate the isolated subgraphs and link them in such that a
     * root-out traversal will get all the nodes.
     */

    psPlot0(ary[0]);

    for (;;) {
	int madeChange = 0;

	for (i = 0; i < n; ++i) {
	    Hash *hash = ary[i];

	    if (hash->ha_DidPlot0) {
		Transit *tran = TransitAry1[hash->ha_Hv & HMASK];
		while (tran) {
		    if (hash == tran->tr_Hash1) {
			Hash *h2 = tran->tr_Hash2;

			if (h2->ha_DidPlot0 == 0 && h2->ha_Parent) {
			    fprintf(stderr, "ADD %s <- %s\n", 
				hash->ha_Name, h2->ha_Name
			    );
			    h2->ha_Parent = hash;
			    psPlot0(h2);
			    madeChange = 1;
			}
		    }
		    tran = tran->tr_Next1;
		}
	    }
	}
	if (!madeChange)
	    break;
    }

    /*
     * Assign volume (ha_Radius) criterea from the root out.  For any
     * given node N, links fanning into the node (A,B,C,...) that are
     * pre-positioned are not counted in that node's radius calculation.
     */

    for (i = 0; i <= 10 && i < n; ++i) {
	ary[i]->ha_PreSet = 1;
    }

    if (n > 0)
	psPlot1(ary[0]);

    /*
     * Assign coordinates for top N nodes (5)
     */

    {
	int r = 0;

	for (i = 1; i <= 5 && i < n; ++i) {
	    Hash *hash = ary[i];
	    r = r + hash->ha_Radius * hash->ha_Radius;
	}
	r = (int)sqrt((double)r) / 1.5;

	if (r < 500.0)
	    r = 500.0;

	for (i = 1; i <= 5 && i < n; ++i) {
	    Hash *hash = ary[i];
	    double theta = M_PI * 2.0 * (double)i / 5.0;

	    hash->ha_PlotX = cos(theta) * r;
	    hash->ha_PlotY = sin(theta) * r;
	    hash->ha_StartTheta = theta - M_PI / 2.0;
	    fprintf(stderr, "PLOT: %s\n", hash->ha_Name);
	}
	r /= 3.0;
	for (i = 6; i <= 10 && i < n; ++i) {
	    Hash *hash = ary[i];
	    double theta = M_PI * 2.0 * (double)(i - 6) / 5.0 + 
		M_PI * 2.0 / 10.0 + M_PI * 2.0 / 20.0;

	    hash->ha_PlotX = cos(theta) * r;
	    hash->ha_PlotY = sin(theta) * r;
	    hash->ha_StartTheta = theta - M_PI / 2.0;
	    fprintf(stderr, "PLOT: %s\n", hash->ha_Name);
	}
    }

    /*
     * Assign actual coordinates and ultimate bounding box
     */

    if (n > 0)
	psPlot2(ary[0]);

    puts("%!PS-Adobe-2.0");
    puts("%%EndComments");
    puts("");

    printf("50 50 translate\n");

    {
	double scaleX = 500.0 / (double)(PlotMaxX - PlotMinX);
	double scaleY = 700.0 / (double)(PlotMaxY - PlotMinY);
	/*
	double scaleX = 2000.0 / (double)(PlotMaxX - PlotMinX);
	double scaleY = 2800.0 / (double)(PlotMaxY - PlotMinY);
	*/

	if (scaleX < scaleY)
	    printf("%4.2f %4.2f scale\n", scaleX, scaleX);
	else
	    printf("%4.2f %4.2f scale\n", scaleY, scaleY);
    }

    printf("%d %d translate\n", -PlotMinX, -PlotMinY);

    printf("/Helvetica findfont 16 scalefont [1 0 0 1 0 0] makefont setfont\n");

    /*
     * Plot the nodes
     */

    for (i = 0; i < n; ++i) {
	psPlot3(ary[i]);
    }
    printf("showpage\n");
}

/*
 * psPlot0(par) - traverse and mark graph.  Link in any isolated
 *		  sub-graphs to guarentee our ability to traverse
 *		  the entire graph from the root node (i.e. us).
 */

void
psPlot0(Hash *par)
{
    Transit *tran;

    /*
     * Mark for traversal and assign base volume criterea
     */

    par->ha_DidPlot0 = 1;

    tran = TransitAry1[par->ha_Hv & HMASK];
    while (tran) {
	if (par == tran->tr_Hash1) {
	    Hash *h2 = tran->tr_Hash2;

	    if (h2->ha_Parent == par && h2->ha_DidPlot0 == 0) {
		psPlot0(h2);
	    }
	}
	tran = tran->tr_Next1;
    }
}

/*
 * psPlot1(par)	- assign volume criterea, angular space,
 *		  and bounding boxes.
 */

void
psPlot1(Hash *par)
{
    Transit *tran;
    int v;
    int asum;
    double r;

    /*
     * calculate centerline circumference
     */

    v = 0;
    tran = TransitAry1[par->ha_Hv & HMASK];
    while (tran) {
	if (par == tran->tr_Hash1) {
	    Hash *h2 = tran->tr_Hash2;

	    if (h2->ha_Parent == par) {
		psPlot1(h2);
		if (h2->ha_PreSet == 0)
		    v += h2->ha_Radius * h2->ha_Radius;
	    }
	}
	tran = tran->tr_Next1;
    }
    ++v;

    /*
     * convert circumference into radius and multiply by two
     * to get radius for 'bounding circle' for par.  This function
     * is definitely not linear.  I have tried log and log-linear but
     * the square root function seems to handle scaling the best.
     */

    asum = v;
    r = sqrt((double)v) + (double)MINRADIUS / 5.0;
    par->ha_Radius = (int)r;

    /*
     * Minimum radius
     */

    if (par->ha_NumLinks < SimplifyFan + 3 && par->ha_Radius < MINRADIUS)
	par->ha_Radius = MINRADIUS;

    /*
     * Make angular assignments.  Each fan-in node gets a cut of the pie
     * based on it's complexity verses the aggregate complexity of all
     * the fan-in nodes in the pie.
     *
     * Pre-placed nodes cover only 180 degrees whereas other nodes cover
     * 360 degrees.  This allows us to tighten the graph bounds a bit
     * more and seems to reduce overlap.
     */

    {
	double theta = M_PI / 8;

	tran = TransitAry1[par->ha_Hv & HMASK];
	while (tran) {
	    if (par == tran->tr_Hash1) {
		Hash *h2 = tran->tr_Hash2;

		if (h2->ha_Parent == par) {
		    double f = (double)(h2->ha_Radius * h2->ha_Radius) / asum;
		    double phi;

		    /*
		     * pre-set nodes fan-out 180 degrees, all other nodes
		     * fan-out 360 degrees.
		     */

		    if (par->ha_PreSet)
			phi = M_PI * 1.0 * f;
		    else
			phi = M_PI * 2.0 * f;

		    if (phi > M_PI / 6.0 && h2->ha_NumLinks <= SimplifyFan + 3)
			phi = M_PI / 6.0;

		    if (h2->ha_PreSet == 0) {
			h2->ha_StartTheta = theta;
			if (h2->ha_NumLinks >= SimplifyFan + 3)
			    h2->ha_StartTheta += phi / 2.0;
			theta += phi;
		    }
		}
	    }
	    tran = tran->tr_Next1;
	}
    }
}

/*
 * psPlot2(par)	- assign actual coordinates
 */

void
psPlot2(Hash *par)
{
    Transit *tran;

    if (par->ha_DidPlot2 < 0) {
	fprintf(stderr, "LOOP ERROR: %s\n", par->ha_Name);
	return;
    }
    if (par->ha_DidPlot2 > 0)
	return;

    par->ha_DidPlot2 = -1;

    /*
     * Assign coordinates
     */

    tran = TransitAry1[par->ha_Hv & HMASK];
    while (tran) {
	if (par == tran->tr_Hash1) {
	    Hash *h2 = tran->tr_Hash2;

	    if (h2->ha_Parent == par) {
		double r = (double)par->ha_Radius / 2.0;

		if (h2->ha_Radius <= MINRADIUS)
		    r /= 2.0;

		if (h2->ha_PlotX == 0 && h2->ha_PlotY == 0) {
		    h2->ha_StartTheta += par->ha_StartTheta;
		    h2->ha_PlotX = par->ha_PlotX + cos(h2->ha_StartTheta) * r;
		    h2->ha_PlotY = par->ha_PlotY + sin(h2->ha_StartTheta) * r;
		}
		psPlot2(h2);

		if (h2->ha_PlotX < PlotMinX)
		    PlotMinX = h2->ha_PlotX;
		if (h2->ha_PlotY < PlotMinY)
		    PlotMinY = h2->ha_PlotY;
		if (h2->ha_PlotX > PlotMaxX)
		    PlotMaxX = h2->ha_PlotX;
		if (h2->ha_PlotY > PlotMaxY)
		    PlotMaxY = h2->ha_PlotY;
	    }
	}
	tran = tran->tr_Next1;
    }
    par->ha_DidPlot2 = 1;
}

/*
 * Plot a hash node - plot all transit chains
 */

void
psPlot3(Hash *hash)
{
    Transit *tran;

    if (hash->ha_DidPlot2 > 0) {
	hash->ha_DidPlot2 = -1;

	for (tran = TransitAry2[hash->ha_Hv & HMASK]; tran; tran = tran->tr_Next2) {
	    if (tran->tr_Hash2 == hash) {
		Hash *h1 = tran->tr_Hash1;

		psPlot3(h1);

		if (h1->ha_DidPlot2 != 0) {
		    double lw = 4.0 *
			log((double)tran->tr_Count + 1.0) /
			log((double)TrMaxCount + 1.0);

		    printf("%4.2f setlinewidth\n", lw);
		    printf("%d %d moveto\n", hash->ha_PlotX, hash->ha_PlotY);
		    printf("%d %d lineto\n", h1->ha_PlotX, h1->ha_PlotY);
		}
	    }
	}
	printf("stroke\n");
	{
	    int w = log((double)hash->ha_Count + 1.0) + 2.0;

	    printf("%d %d %d %d rectfill\n", 
		hash->ha_PlotX - w, 
		hash->ha_PlotY - w, 
		w * 2, 
		w * 2
	    );

	    if (hash->ha_NumLinks < SimplifyFan + 3) {
		printf("%d %d translate %d rotate %d 0 moveto (%s) show %d rotate %d %d translate\n",
		    hash->ha_PlotX,
		    hash->ha_PlotY,
		    (int)(hash->ha_StartTheta * 360 / (M_PI * 2.0)),
		    w + 2,
		    hash->ha_Name,
		    -(int)(hash->ha_StartTheta * 360 / (M_PI * 2.0)),
		    -hash->ha_PlotX,
		    -hash->ha_PlotY
		);
	    } else {
		printf("%d %d moveto (%s) show\n",
		    hash->ha_PlotX + w + 2,
		    hash->ha_PlotY,
		    hash->ha_Name
		);
	    }
	}
    }
}

/*
 * By bulk
 */

int
psQSortA(const void *h1, const void *h2)
{
    Hash *hash1 = *(Hash **)h1;
    Hash *hash2 = *(Hash **)h2;

    return(hash2->ha_Count - hash1->ha_Count);
}

/*
 * By fan-in
 */

int
psQSortB(const void *h1, const void *h2)
{
    Hash *hash1 = *(Hash **)h1;
    Hash *hash2 = *(Hash **)h2;

    if (hash2->ha_NumLinks != hash1->ha_NumLinks)
	return(hash2->ha_NumLinks - hash1->ha_NumLinks);
    else
	return(hash2->ha_Count - hash1->ha_Count);
}

