# -*- perl -*-

#
# $Id: Main.pm,v 1.11 2003/11/11 22:34:28 eserte Exp $
# Author: Slaven Rezic
#
# Copyright (C) 2001,2002 Slaven Rezic. All rights reserved.
#
# Mail: slaven@rezic.de
# WWW:  http://bbbike.sourceforge.net
#

package ESRI::Shapefile::Main;

use strict;

use constant FILECODE => 9994;
use constant VERSION => 1000;

BEGIN {
    for (["Null"       => 0], # Null Shape 
	 ["Point"      => 1],
	 ["PolyLine"   => 3],
	 ["Polygon"    => 5],
	 ["MultiPoint" => 8],
	 ["PointZ"     => 11],
	 ["PolyLineZ"  => 13],
	 ["PolygonZ"   => 15],
	 ["MultiPointZ"=> 18],
	 ["PointM"     => 21],
	 ["PolyLineM"  => 23],
	 ["PolygonM"   => 25],
	 ["MultiPointM"=> 28],
	 ["MultiPatch" => 31],
	) {
	(my $shape_type = $_->[0]) =~ s/\W//g;
	my $cmd = 'use constant SHAPE_'.uc($shape_type).' => '.$_->[1];
	#warn $cmd;
	eval $cmd;
    }
}

use Class::Accessor; # workaround 5.00503 bug (???)
use base qw(Class::Accessor::Fast);

__PACKAGE__->mk_accessors(qw/File Root Header Records FH Init/);

sub new {
    my $this = shift;
    my $class = ref($this) || $this;
    my $self = {};
    bless $self, $class;

    my(%args) = @_;

    if ($args{-root}) {
	$self->Root($args{-root});
    }
    if ($args{-file}) {
	$self->File($args{-file});
    }
    $self->Init(0);

    $self;
}

# -force => 1: force initialization (useful for -nopreload => 1 and next_record)
sub init {
    my($self, %args) = @_;
    if (!$self->Init || $args{'-force'}) {
	$self->set_file($self->File, %args);
	$self->Init(1);
    }
}

# -nopreload => 1: do not preload Records member ... the user can
#    iterate with the next_record method or preload himself by using
#    preload_records
sub set_file {
    my($self, $file, %args) = @_;

    $self->File($file);

    my $fh;
    if ($] < 5.006) {
	require Symbol;
	$fh = Symbol::gensym();
    }
    open($fh, $file) or die "Can't open $file: $!";
    $self->FH($fh);
    binmode $fh;

    read $fh, my($header), 100;
    $self->Header(ESRI::Shapefile::Main::Header->new($header));

    $self->preload_records unless $args{'-nopreload'};

}

sub preload_records {
    my $self = shift;
    my $fh = $self->FH;

    my @records;
    while(!eof $fh) {
	push @records, ESRI::Shapefile::Main::Record->new($self);
    }
    $self->Records(\@records);

    close $fh;
    $self->FH(undef);
}

sub next_record {
    my $self = shift;
    if (!defined $self->FH) {
	require Carp; Carp::cluck(); # XXX debugging
	warn "File descriptor already closed";
	return;
    }
    if (eof $self->FH) {
	close $self->FH;
	$self->FH(undef);
	return undef;
    }
    ESRI::Shapefile::Main::Record->new($self);
}

######################################################################

package ESRI::Shapefile::Main::Header;

use base qw(Class::Accessor::Fast);

__PACKAGE__->mk_accessors(qw/FileCode FileLength Version ShapeType BoundingBox/);

sub new {
    my $this = shift;
    my $class = ref($this) || $this;
    my $self = {};
    bless $self, $class;

    my $buf = shift;

    $self->FileCode(unpack("N", substr($buf, 0, 4)));
    die "Wrong file code @{ $self->FileCode }"
	if $self->FileCode != ESRI::Shapefile::Main::FILECODE;
    $self->FileLength(unpack("N", substr($buf, 24, 4)));
    $self->Version(unpack("V", substr($buf, 28, 4)));
    die "Wrong version @{ $self->Version }"
	if $self->Version != ESRI::Shapefile::Main::VERSION;
    $self->ShapeType(unpack("V", substr($buf, 32, 4)));

    my %bounding_box;
    my $pos = 36;
    for (qw(Xmin Ymin Xmax Ymax Zmin Zmax Mmin Mmax)) {
	# XXX d on Bigendian?
	$bounding_box{$_} = unpack("d", substr($buf, $pos, 8));
	$pos+=8;
    }
    $self->BoundingBox(\%bounding_box);
    $self;
}

######################################################################

package ESRI::Shapefile::Main::Record;

use base qw(Class::Accessor::Fast);

__PACKAGE__->mk_accessors(qw(RecordNumber ContentLength ShapeType));

sub new {
    my $class = shift;
    my $root  = shift;

    read $root->FH, my($header), 12;

    my $shape_type = unpack("V", substr($header, 8, 4));

    my $new_class =
	{ ESRI::Shapefile::Main::SHAPE_POINT()    => "Point",
	  ESRI::Shapefile::Main::SHAPE_POLYLINE() => "PolyLine",
	  ESRI::Shapefile::Main::SHAPE_POLYGON()  => "Polygon",
	  ESRI::Shapefile::Main::SHAPE_NULL()     => "Null",
	  #XXX
	}->{$shape_type};
    if (!defined $new_class) {
	die "Unhandled shape type $shape_type at pos " . tell($root->FH);
    }
    $new_class = "ESRI::Shapefile::Main::Record::" . $new_class;

    my $record = $new_class->new_from_file($root);

    $record->RecordNumber(unpack("N", substr($header, 0, 4)));
    # here, ContentLength is in Bytes
    $record->ContentLength(unpack("N", substr($header, 4, 4)) * 2);
    $record->ShapeType($shape_type);

    $record;
}

######################################################################

package ESRI::Shapefile::Main::Record::Null;

use base qw(ESRI::Shapefile::Main::Record Class::Accessor::Fast);

sub new_from_file {
    my $this = shift;
    my $class = ref($this) || $this;
    my $self = {};
    bless $self, $class;
}

######################################################################

package ESRI::Shapefile::Main::Record::Point;

use base qw(ESRI::Shapefile::Main::Record Class::Accessor::Fast);

__PACKAGE__->mk_accessors(qw(Point));

sub new_from_file {
    my $this = shift;
    my $class = ref($this) || $this;
    my $self = {};
    bless $self, $class;

    my $root = shift;

    read $root->FH, my($header), 16;

    $self->Point([unpack("d2", $header)]);

    $self;
}

######################################################################

package ESRI::Shapefile::Main::Record::PolyLine;

use base qw(ESRI::Shapefile::Main::Record Class::Accessor::Fast);

__PACKAGE__->mk_accessors(qw(Box Lines));

sub new_from_file {
    my $this = shift;
    my $class = ref($this) || $this;
    my $self = {};
    bless $self, $class;

    my $root = shift;

    read $root->FH, my($header), 40;

    # XXX big endian?
    $self->Box([unpack("d4", substr($header, 0, 32))]);

    my $num_parts  = unpack("V", substr($header, 32, 4));
    my $num_points = unpack("V", substr($header, 36, 4));

    my $parts_len = $num_parts*4;
    read $root->FH, $header, $parts_len;
    my(@parts)  = unpack("V$num_parts", $header);

    # 8 is length of a double
    read $root->FH, $header, $num_points*8*2;
    # XXX big endian?
    my(@points) = map { [ unpack("d".2,$_) ] } unpack("A16"x($num_points), $header);
    my @lines;
    for my $part_i (0 .. $#parts) {
	my $end_index = ($part_i < $#parts ? $parts[$part_i+1]-1 : $#points);
	my @line;
	for my $i ($parts[$part_i] .. $end_index) {
	    push @line, $points[$i];
	}
	push @lines, \@line;
    }

    $self->Lines(\@lines);

    $self;
}

######################################################################

package ESRI::Shapefile::Main::Record::Polygon;

use base qw(ESRI::Shapefile::Main::Record Class::Accessor::Fast);

__PACKAGE__->mk_accessors(qw(Box Areas));

sub new_from_file {
    my $this = shift;
    my $class = ref($this) || $this;
    my $self = {};
    bless $self, $class;

    my $root = shift;

    read $root->FH, my($header), 40;

    # XXX big endian?
    $self->Box([unpack("d4", substr($header, 0, 32))]);

    my $num_parts  = unpack("V", substr($header, 32, 4));
    my $num_points = unpack("V", substr($header, 36, 4));

    my $parts_len = $num_parts*4;
    read $root->FH, $header, $parts_len;
    my(@parts)  = unpack("V$num_parts", $header);

    # 8 is length of a double
    read $root->FH, $header, $num_points*8*2;
    # XXX big endian?
    my(@points) = map { [ unpack("d".2,$_) ] } unpack("A16"x($num_points), $header);
    my @lines;
    for my $part_i (0 .. $#parts) {
	my $end_index = ($part_i < $#parts ? $parts[$part_i+1]-1 : $#points);
	my @line;
	for my $i ($parts[$part_i] .. $end_index) {
	    push @line, $points[$i];
	}
	push @lines, \@line;
    }

    $self->Areas(\@lines);

    $self;
}


1;

__END__
