Cyber EarthのPerl-20110713版

Cyber EarthPerl初版(id:kazkun:20110711)を修正、キャッシュモード用CGIも移植し、一応の完成になりました。
日記にソースをウダウダ書くのもどうかと思ったんですけど、面倒なのでここに全コード貼りつけておきます。そのうちcode.googleとかgithubとか考えます。

cyber-earth.cgi

#!/usr/bin/perl
# Cyber Earth - The CyberJapan Map layer CGI for Google Earth.
# 
# Gauche version is written by YOKOTA Hiroshi 2008-07-28
#       http://www.netlab.is.tsukuba.ac.jp/~yokota/izumi/cyber-earth/
# Perl version is ported by Kazkun
# 
# Copyright pending because there is no response from Original author YOKOTA to date.
#
# 20110711 Kaz : Initial release on Perl
#                ADD ALPHA parameter, MODE parameter
#                MOD fixed for new format of CyberJapan site
# 20110713 Kaz : Cleanup code.

use strict;
use warnings;
use POSIX;
use CGI;
use List::Util qw(max min);

my $MYURLBASE = "http://localhost/cyberearth/";

sub toSecond100 {
	my ($val) = @_;
	return $val * (60 * 60 * 100.0);
}

sub toDegree {
	my ($val) = @_;
	return $val / (60 * 60 * 100.0);
}

sub isAvailableArea {
	my ($lat, $lon, $level) = @_;
	return 0 if $lon < toSecond100(121.0) - $level || $lon > toSecond100(154.0);
	return 0 if $lat < toSecond100( 20.2) - $level || $lat > toSecond100( 46.5);
	return 1;
}

sub makePosList {
	my ($start, $limit, $step) = @_;
	my ($i, @list);
	for($i = $start; $i < $limit; $i += $step) {
		unshift(@list, $i);
	}
	return @list;
}

sub adjustLevel {
	my ($lv) = @_;
	# avoid range bug. GoogleEarth puts wrong range value when the eye is very far from the earth. (Ver.4.1.7076.4458/Linux)
	return 3072000 if $lv <=       0;
	return     750 if $lv <=     750;
	return    1500 if $lv <=    1500;
	return    3000 if $lv <=    3000;
	return    6000 if $lv <=    6000;
	return   12000 if $lv <=   12000;
	return   24000 if $lv <=   24000;
	return   48000 if $lv <=   48000;
	return   96000 if $lv <=   96000;
	return  192000 if $lv <=  192000;
	return  384000 if $lv <=  384000;
	return  768000 if $lv <=  768000;
	return 1536000 if $lv <= 1536000;
	return 3072000;
	
}

sub levToLevID {
	my($lv) = @_;
	return "7.5fgd"       if $lv <=     750;
	return "15nti"        if $lv <=    1500;
	return "30nti"        if $lv <=    3000;
	return "60nti"        if $lv <=    6000;
	return "120bafd"      if $lv <=   12000;
	return "240bafd"      if $lv <=   24000;
	return "480bafd"      if $lv <=   48000;
	return "960bafd"      if $lv <=   96000;
	return "1920bafd"     if $lv <=  192000;
	return "raster/3840"  if $lv <=  384000;
	return "raster/7680"  if $lv <=  768000;
	return "raster/15360" if $lv <= 1536000;
	return "raster/30720";
}


# main logic starts here

my $query = new CGI;
my $mode = 0;
if ($query->param('MODE') == '1') {
	$mode = 1;
}
my $alpha = $query->param('ALPHA') + 0;
$alpha = 220 if ($alpha == 0);
my ($wdeg, $sdeg, $edeg, $ndeg) = split(/,/, $query->param('BBOX'));
my $wsec = toSecond100($wdeg + 0);
my $ssec = toSecond100($sdeg + 0);
my $esec = toSecond100($edeg + 0);
my $nsec = toSecond100($ndeg + 0);
my $lonsec = toSecond100($query->param('lookatLon') + 0);
my $latsec = toSecond100($query->param('lookatLat') + 0);
my $range = $query->param('lookatRange') + 0;
my $tilt = $query->param('lookatTilt') + 0;

# decide map level
my $level;
if ($tilt < 20) {
	$level = adjustLevel(max(abs($esec - $wsec), abs ($nsec - $ssec)) / 6);
} else {
	$level = adjustLevel($range / 4 * 3);		# FIXME : use $tilt
}

# reshape BoundBox
$wsec = max((floor($wsec/$level) - 1) * $level, (floor($lonsec/$level) - 3) * $level, toSecond100(0)),
$ssec = max((floor($ssec/$level) - 1) * $level, (floor($latsec/$level) - 3) * $level, toSecond100(-90)),
$esec = min((floor($esec/$level) + 1) * $level, (floor($lonsec/$level) + 4) * $level, toSecond100(360)),
$nsec = min((floor($nsec/$level) + 1) * $level, (floor($latsec/$level) + 4) * $level, toSecond100(90)), 

# build overlay position list
my @latlist = makePosList($ssec, $nsec, $level);
my @lonlist = makePosList($wsec, $esec, $level);
my @gridlist = ();
my $lat;
my $lon;
foreach $lat (@latlist) {
	foreach $lon (@lonlist) {
		push(@gridlist, [$lat, $lon]) if isAvailableArea($lat, $lon, $level);
	}
}

# setup misc data
my $color = sprintf("%02x", $alpha)."ffffff";
my $levstr = levToLevID($level);

# output XML data
print "Content-type: application/vnd.google-earth.kml+xml\n";
print "\n";
print "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
print "<kml xmlns=\"http://earth.google.com/kml/2.0\">\n";
print "  <Document>\n";
print "    <Folder>\n";
print "      <name>layers</name>\n";
print "      <open>0</open>\n";
print "      <description>";
	print "MODE:" . ($mode == 1 ? "CACHED" : "DIRECT");
	print " ALPHA:$alpha";
	print " N:$ndeg E:$edeg W:$wdeg S:$sdeg Lev:$level";
	print " Nbox:" . toDegree($nsec);
	print " Ebox:" . toDegree($esec);
	print " Wbox:" . toDegree($wsec);
	print " Sbox:" . toDegree($ssec);
	print " lr:" . $query->param('lookatRange');
	print " llat:" . $query->param('lookatLat');
	print " llon:" . $query->param('lookatLon');
	print " lt:" . $query->param('lookatTilt');
print "</description>\n";

# output Overlay elements
my $pos;
foreach $pos (@gridlist) {
	my $lat = $pos->[0];
	my $lon = $pos->[1];
	my $path = "/data/$levstr/new/$lon/$lon-$lat-img.png";
	my $url;
	if ($mode < 1) {
		$url = "http://cyberjapandata.gsi.go.jp$path";
	} else {
		$url = "${MYURLBASE}cyber-earth-cache.cgi?filename=$path";
	}
	my $n = toDegree($lat + $level);
	my $s = toDegree($lat);
	my $e = toDegree($lon + $level);
	my $w = toDegree($lon);
	print "      <GroundOverlay>\n";
	print "        <name>$level:$lat:$lon</name>\n";
	print "        <color>$color</color>\n";
	print "        <Icon>\n";
	print "          <href>$url</href>\n";
	print "        </Icon>\n";
	print "        <LatLonBox>\n";
	print "          <north>$n</north>\n";
	print "          <south>$s</south>\n";
	print "          <east>$e</east>\n";
	print "          <west>$w</west>\n";
	print "        </LatLonBox>\n";
	print "      </GroundOverlay>\n";
}
print "    </Folder>\n";
print "  </Document>\n";
print "</kml>\n";
exit 0;

cyber-earth-cache.cgi

#!/usr/bin/perl
# Cyber Earth cache CGI.
# 
# Gauche version is written by YOKOTA Hiroshi 2008-07-28
#       http://www.netlab.is.tsukuba.ac.jp/~yokota/izumi/cyber-earth/
# Perl version is ported by Kazkun 2011-07-13
# 
# Copyright pending because there is no response from Original author YOKOTA to date.
#
# 20110713 Kaz : Initial release on Perl
#                MOD fixed for new format of CyberJapan site
#                MOD changed cache path to /tmp/FileCache/cyberearth
#                ADD ability to purge cache file automatically

use strict;
use warnings;
use CGI;
use LWP::UserAgent;
use MIME::Base64;
use Cache::SizeAwareFileCache;	# Cache-Cache module see http://search.cpan.org/~jswartz/Cache-Cache-1.06/

my $cache = Cache::SizeAwareFileCache->new({
#				'cache_root' => '/var/www/cyberearth/cache',	# cache dir
				'namespace' => 'cyberearth',
				'default_expires_in' => 60*60*2,	# two hours to expire
				'max_size' => 1024*1024*30			# 30Mbytes max
				});
my $query = new CGI;
my $ua = LWP::UserAgent->new;
$ua->agent("CyberEarth-cache/1.0p");				# name of User-Agent
#$ua->proxy('http', 'http://127.0.0.1:8080');		# uncomment and modify if use proxy server

my $isOfflineMode = -e "./cyber-earth-offline";		# if exists the file, this CGI becomes offline mode

# dummy image data encoded by base64
my $pngBroken = decode_base64("iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIEAYAAACUn2LIAAAABmJLR0T///////8JWPfcAAAACXBIWXMAAABIAAAASABGyWs+AAAAOklEQVQoz2P8/5+BgYEBQpIDWNAFGBkRJDaAYSG6AC4X4VKH0wXoGnC5jIlcv1PNC0Q7FZdBjJRGIwCIUDfmr9EyzAAAAABJRU5ErkJggg==");
my $pngOffline = decode_base64("iVBORw0KGgoAAAANSUhEUgAAAAQAAAAEEAYAAAD5YUI9AAAABmJLR0T///////8JWPfcAAAACXBIWXMAAABIAAAASABGyWs+AAAAF0lEQVQI12NggIL//xEkCh+nBDog2wQAOLwf4aFzXgQAAAAASUVORK5CYII=");

sub getHttpData {
	my ($uri) = @_;
	my ($req, $res);
	$req = HTTP::Request->new(GET => $uri);
#	$req->header('Accept' => 'image/png');
	$res = $ua->request($req);
	return ($res->code, $res->content);
}

sub isValidPath {
	my ($filename) = @_;
	return 0 if (! defined $filename);
	return 1 if ($filename =~ m/^\/data\/(raster\/)?[0-9.]+[a-z]*\/new\/[0-9]+\/[0-9]+-[0-9]+-img\.png$/);
	return 0;
}

# make dummy image data
sub responseDummyImage {
	my ($img) = @_;
	my $pngsize = length($img);
		print "Status: 200 replacement image\n";
		print "Content-type: image/png\n";
		print "Content-Length: $pngsize\n";
		print "\n";
		print $img;
}

sub isPng {
	my ($data) = @_;
	return 1 if ($data =~ m/^\x89PNG/);
	return 0;
}

sub responseRequest {
	my ($path) = @_;
	if (! isValidPath($path)) {
		print "Status: 403 filename invalid\n";
		print "Content-type: text/plain\n";
		print "\n";
		print "filename invalid\n";
		return;
	}
	my $data = $cache->get($path);
	if (defined $data) {
		my $pngsize = length($data);
		print "Status: 200 cached\n";
		print "Content-type: image/png\n";
		print "Content-Length: $pngsize\n";
		print "\n";
		print $data;
		return;
	}
	if ($isOfflineMode) {
		responseDummyImage($pngOffline);
		return;
	}
	my ($status, $body) = getHttpData("http://cyberjapandata.gsi.go.jp$path");
	if ($status != 200 || length($body) == 0 || ! isPng($body)) {
		responseDummyImage($pngBroken);
		return;
	}
	my $pngsize = length($body);
	print "Status: 200 ok\n";
	print "Content-type: image/png\n";
	print "Content-Length: $pngsize\n";
	print "\n";
	print $body;
	$cache->set($path, $body);
	return;
}

# main logic starts here

my $path = $query->param('filename');
responseRequest($path);
close(STDOUT);							# close the connection from google Earth
$cache->Purge if (rand(1) < 0.05);		# execute the Purge probability of 5%
exit 0;

ということで、就活行ってきます。えぇ、お仕事募集中ですよ。もちろん。