#!/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 ) ); }