#!/usr/bin/perl
# Perl script to show the access of the otago eprints archive by city on a 2D map
# accessible by any web browser
# Author: Nigel Stanger
#		  altered by Hayden Kane December 2007
use strict;
use Time::HiRes qw( gettimeofday );
use CGI;
use DBI;
use GD;
use Geo::Proj4;
#################################### Declaration of Variables ####################################
# Timing Variables
my ( $start_sec, $start_micro ) = gettimeofday;
my ($start_time) = ( $start_sec * 1000 ) + round( $start_micro / 1000 );
my ($page);

# Database connection. NOTE: Ensure these are accurate to connect to your database
my ($dsn)       = "DBI:mysql:database=eprint2;host=127.0.0.1";
my ($user_name) = "eprint2pub";
my ($password)  = "public";
my ( $connect, $query, %types, %unmapped, $stat, $row, $num_rows, $vtype );
my ($where) = '';

# GD image stuff.
my ( $mapimage, $mapimagefile );
my ( $red,      $white, $black, $blue, $tc );
my ( $width,    $height );

# Miscellaneous variables.
my ($x_offset) = 16986796.16;
my ($y_offset) = 8615499.05;
my ($max_x)    = $x_offset * 2;
my ($max_y)    = $y_offset * 2;
my ( %cities, %IPs );
my ($num_entries) = -1
  ; # include all entries from database set a positive value to limit result size for debugging
my ($num_hits) = 0;
my ( $ip, $count, $location );
my ( $city, $country, $region, $lat, $long, $x, $y ) =
  ( '', '', '', 0, 0, 0, 0 );

# uncomment one of the next three lines only depending on what information you wish to show
#my ($show_only) = 'abstract';   # show views of abstracts only
#my ($show_only) = 'download';   # show views of downloads only
my ($show_only) = 'both';    # include both abstracts & downloads
my ($eprint)    = '';
my ($proj);
#################################### Initial Setup of Parameters ####################################
# set up CGI parameters, including the header so that the information can be recognised
$page = new CGI;
print $page->header(
					 -type          => "image/jpeg",
					 -Pragma        => 'no-cache',
					 -Cache-Control => 'no-cache'
);

# set up Proj4 parameters for this project
$proj = Geo::Proj4->new( proj => "robin", ellps => "sphere", lon_0 => 10 )
  or die "parameter error: " . Geo::Proj4->error . "\n";
$width    = 1024;
$height   = 520;
$mapimage = new GD::Image( $width, $height );
$white    = $mapimage->colorAllocate( 255, 255, 255 );
$black    = $mapimage->colorAllocate( 0, 0, 0 );
$red      = $mapimage->colorAllocate( 255, 0, 0 );
$blue     = $mapimage->colorAllocate( 0, 0, 255 );
for ( my $i = 1 ; $i < 255 ; $i++ ) {
	$tc = $mapimage->colorAllocate( $i, 0, ( 255 - $i ) );
}
$mapimage->transparent($white);

# set up a connection to the database
$connect = DBI->connect( $dsn, $user_name, $password, { RaiseError => 1 } );
$types{'download'}    = $types{'abstract'}    = 0;
$unmapped{'download'} = $unmapped{'abstract'} = 0;

# Set up query to reflect the requested information based on previous parameters
# type of data shown
if ( $show_only eq 'both' ) {
	$where = "view_type IN ('download', 'abstract')";
} else {
	$where = "view_type = '$show_only'";
}

# Uncomment the next line to remove the data that pertains to the search engines (Yahoo!, Google, etc)
#$where .= " and country_code not like 'X@%'";
# Create the query, and execute it
$query =
"SELECT ip, view_type, city, region, country_name, latitude, longitude, COUNT(*) AS count
	 FROM view
	 WHERE $where
	 GROUP BY ip, view_type
	 ORDER BY count DESC" . ( ( $num_entries > 0 ) ? " LIMIT $num_entries" : '' );
$stat = $connect->prepare($query);
$stat->execute();
$num_rows = $stat->rows;
#################################### Main Part of Script ####################################
# If the query returns some results, they need to be processed
if ( $num_rows > 0 ) {
	$num_entries = $num_rows if ( $num_entries < 1 );

	# Process each individual row returned
	while ( $row = $stat->fetchrow_hashref() ) {
		$ip       = $row->{'ip'};
		$count    = $row->{'count'};
		$vtype    = $row->{'view_type'};
		$IPs{$ip} = 1;
		$lat      = $row->{'latitude'};
		$long     = $row->{'longitude'};
		$country  = $row->{'country_name'};
		$region   = $row->{'region'};
		$city = (
				  ( $row->{'city'} eq '' )
				  ? 'Unknown'
				  : $row->{'city'}
		  )
		  . "_"
		  . $region . "_"
		  . $country;

# Transform the longitude and latitude values to x,y co-ordinated to place on the map image
		( $x, $y ) = $proj->forward( $lat, $long );
		$x = round( ( $x + $x_offset ) / $max_x * $width );
		$y = round( ( $y_offset - $y ) / $max_y * $height );

# If this city hasn't already been defined, create an entry for it in the cities Map data structure
		if ( !defined( $cities{$city} ) ) {
			$cities{$city}{'lat'}      = $lat;
			$cities{$city}{'long'}     = $long;
			$cities{$city}{'abstract'} = 0;
			$cities{$city}{'download'} = 0;
			$cities{$city}{'count'}    = 0;
		}

  # Update the values for the city based on the infomation collected in this row
		$cities{$city}{$vtype}  += $count;
		$cities{$city}{'count'} += $count;
		$types{$vtype}          += $count;
		$cities{$city}{'x'} = $x;
		$cities{$city}{'y'} = $y;
	}
}

# close the connection to the database
$stat->finish();
$connect->disconnect();

# Generate dots for each city.
CITY: foreach $city ( keys %cities ) {
	if ( $show_only eq 'both' ) {

		# Blend colour according to the ratio of abstracts to downloads.
		$tc = $mapimage->colorClosest(
			round( $cities{$city}{'download'} / $cities{$city}{'count'} * 255 ),
			0,
			round( $cities{$city}{'abstract'} / $cities{$city}{'count'} * 255 )
		);
	} elsif ( $show_only eq 'download' ) {
		next CITY if ( $cities{$city}{'download'} == 0 );
		$tc = $red;
	} elsif ( $show_only eq 'abstract' ) {
		next CITY if ( $cities{$city}{'abstract'} == 0 );
		$tc = $blue;
	} else    # ack, boom
	{
		last CITY;
	}

	# Scale the size of the dotes based upon number of accesses

	# Uncomment one of the following lines only. For best results use the first if including the search engine infomation, the second if not
	my $projection = 0.0002;
	#my $projection = 0.004;
	
	my $count = 0;
	$count = $cities{$city}{'count'};
	$count = $projection * $count;
	$mapimage->filledEllipse($cities{$city}{'x'},$cities{$city}{'y'}, $count, $count, $tc);
}

# Output summary data.
if ( ( $show_only eq 'both' ) || ( $show_only eq 'download' ) ) {
	
	# Downloads summary Data
	$mapimage->string( gdSmallFont,
					   3, 3,
					   "$types{'download'} downloads"
						 . (
							 ( $unmapped{'download'} > 0 )
							 ? " (+$unmapped{'download'} unmappable)"
							 : ''
						 ),
					   $red
	);
}
if ( ( $show_only eq 'both' ) || ( $show_only eq 'abstract' ) ) {
	# Abstracts summary Data
	$mapimage->string( gdSmallFont,
					   3, 15,
					   "$types{'abstract'} abstracts"
						 . (
							 ( $unmapped{'abstract'} > 0 )
							 ? " (+$unmapped{'abstract'} unmappable)"
							 : ''
						 ),
					   $blue
	);
}
# Cities summary Data
$mapimage->string( gdSmallFont, 3, 27,
				   'from ' . scalar( keys %cities ) . ' cities', $black );

# IP summary Data
$mapimage->string( gdSmallFont, 3, 39,
				   '(' . scalar( keys %IPs ) . ' IP addresses)', $black );

# Time taken summary Data
my ( $finish_sec, $finish_micro ) = gettimeofday();
my ($finish_time) = ( $finish_sec * 1000 ) + round( $finish_micro / 1000 );
$mapimage->string( gdSmallFont, 3,
				   $height - 15,
				   'Map generated in ' . ( $finish_time - $start_time ) . ' ms',
				   $black );

binmode(STDOUT);
print $mapimage->png();


# routine to round to the nearest whole number
sub round {
	my ($n) = shift;
	return int( $n + 0.5 * ( $n <=> 0 ) );
}
