#!/usr/bin/env perl use strict; use Time::HiRes qw( gettimeofday ); use CGI; use DBI; use GD; use Geo::IP; use Geo::Proj4; my ( $start_sec, $start_micro ) = gettimeofday; my ($start_time) = ( $start_sec * 1000 ) + round( $start_micro / 1000 ); my ($page); # Database connection. my ($dsn) = "DBI:mysql:database=eprintstats;host=localhost"; my ($user_name) = "eprintstatspriv"; my ($password) = "AuldGrizzel"; 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 ); # Geolocation database. my ( $gi, $proj ); my ($gidb) = '/usr/local/share/GeoIP/GeoLiteCity.dat'; # 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 my ($num_hits) = 0; my ( $ip, $count, $location ); my ( $city, $lat, $long, $x, $y ) = ( 0, 0, '', 0, 0 ); my ($show_only) = 'both'; # include both abstracts & downloads my ($eprint) = ''; $page = new CGI; print $page->header( -type => "image/jpeg", -Pragma => 'no-cache', -Cache-Control => 'no-cache' ); $num_entries = $page->param('top') if ( defined $page->param('top') ); $show_only = $page->param('show') if ( defined $page->param('show') ); $eprint = $page->param('eprint') if ( defined $page->param('eprint') ); $gi = Geo::IP->open( $gidb, GEOIP_STANDARD ) or die "Unable to open GeoIP database $gidb\n"; $proj = Geo::Proj4->new( proj => "robin", ellps => "sphere", lon_0 => 10 ) or die "parameter error: " . Geo::Proj4->error . "\n"; $width = $page->param('width'); $height = $page->param('height'); $mapimagefile = "/Users/nstanger/Sites/maps/map_${width}x${height}.png"; $mapimage = GD::Image->newFromPng( $mapimagefile, 1 ); $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 ) ); } $connect = DBI->connect( $dsn, $user_name, $password, { RaiseError => 1 } ); $types{'download'} = $types{'abstract'} = 0; $unmapped{'download'} = $unmapped{'abstract'} = 0; # Set up query. if ( $show_only eq 'both') { $where = "view_type IN ('download', 'abstract')"; } else { $where = "view_type = '$show_only'"; } $where .= " AND archiveid IN ($eprint)" unless ( $eprint eq '' ); $query = "SELECT ip, view_type, 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; if ( $num_rows > 0 ) { $num_entries = $num_rows if ( $num_entries < 1 ); while ( $row = $stat->fetchrow_hashref() ) { $ip = $row->{'ip'}; $count = $row->{'count'}; $vtype = $row->{'view_type'}; $IPs{$ip} = 1; $location = $gi->record_by_addr($ip); if ( defined($location) ) { $lat = $location->latitude; $long = $location->longitude; $city = ( ( $location->city eq '' ) ? 'Unknown' : $location->city ) . " ($lat, $long)"; ( $x, $y ) = $proj->forward( $lat, $long ); $x = round( ( $x + $x_offset ) / $max_x * $width ); $y = round( ( $y_offset - $y ) / $max_y * $height ); if ( !defined( $cities{$city} ) ) { $cities{$city}{'lat'} = $lat; $cities{$city}{'long'} = $long; $cities{$city}{'abstract'} = 0; $cities{$city}{'download'} = 0; $cities{$city}{'count'} = 0; } $cities{$city}{$vtype} += $count; $cities{$city}{'count'} += $count; $types{$vtype} += $count; $cities{$city}{'x'} = $x; $cities{$city}{'y'} = $y; } else { $unmapped{$vtype} += $count; } } } $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; } $mapimage->filledRectangle( $cities{$city}{'x'} - 1, $cities{$city}{'y'} - 1, $cities{$city}{'x'} + 1, $cities{$city}{'y'} + 1, $tc ); } # Output summary data. if ( ( $show_only eq 'both' ) || ( $show_only eq 'download' ) ) { $mapimage->string( gdSmallFont, 3, 3, "$types{'download'} downloads" . ( ( $unmapped{'download'} > 0 ) ? " (+$unmapped{'download'} unmappable)" : '' ), $red ); } if ( ( $show_only eq 'both' ) || ( $show_only eq 'abstract' ) ) { $mapimage->string( gdSmallFont, 3, 15, "$types{'abstract'} abstracts" . ( ( $unmapped{'abstract'} > 0 ) ? " (+$unmapped{'abstract'} unmappable)" : '' ), $blue ); } $mapimage->string( gdSmallFont, 3, 27, 'from ' . scalar( keys %cities ) . ' cities', $black ); $mapimage->string( gdSmallFont, 3, 39, '(' . scalar( keys %IPs ) . ' IP addresses)', $black ); 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 ); open RESULTS, ">>/tmp/gd_results_$num_entries.txt" or die "Argh!\n"; print RESULTS ( $finish_time - $start_time ) . "\n"; close RESULTS; binmode(STDOUT); print $mapimage->jpeg(); sub round { my ($n) = shift; return int( $n + 0.5 * ( $n <=> 0 ) ); }