The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# $Id: ShapeFile.pm,v 1.15 2004/08/21 04:13:28 asc Exp $
use strict;

package XML::Generator::SVG::ShapeFile;
use base qw (XML::SAX::Base);

$XML::Generator::SVG::ShapeFile::VERSION = '0.2';

=head1 NAME

XML::Generator::SVG::ShapeFile - Generate SAX2 events for an SVG rendering of an ESRI shapefile.

=head1 SYNOPSIS

 use PerlIO::gzip;
 use XML::SAX::Writer;
 use XML::Generator::SVG::ShapeFile;

 # see CAVEATS below

 open SVGZ, ">:gzip", "/path/to/my/output.svgz"
    || die "do the right thing, luke";

 my $writer = XML::SAX::Writer->new(Output => \*SVGZ);
 my $svg    = XML::Generator::SVG::ShapeFile->new(Handler=>$writer);

 $svg->set_width(1024);
 $svg->set_decimals(1);

 $svg->set_title("You are here");
 $svg->set_stylesheet("foo.css");

 $svg->add_point({lat=>"123",long=>"456"});

 $svg->render("/path/to/shapefile");

=head1 DESCRIPTION

Generate SAX2 events for an SVG rendering of an ESRI shapefile.

=head1 CAVEATS

Depending on your input data, this package may generate huge
SVG files if left uncompressed.

=head1 DOCUMENT STRUCTURE

 + svg

   + metadata
     + rdf:Description [@rdf:about = '...']
       ~ dc:title
       ~ dc:description
       ~ dc:publisher
       ~ dc:language
       - dc:date
       - dc:format

   + g [@id = 'map'] 
     - rect [@id = 'canvas']
     - path                       (+)

   ~ g [@id = 'locations']

     + g [@id = '...']            (+)
       - title
       -circle

=cut

use Geo::ShapeFile;
use Date::Simple;

=head1 PACKAGE METHODS

=cut

=head2 __PACKAGE__->new(\%args)

Inherits from XML::SAX::Base, so constructor arguments
are the same.

=cut

sub new {
    my $pkg = shift;

    my $self = $pkg->SUPER::new(@_);

    $self->{'__points'}   = [];
    $self->{'__metadata'} = {};

    $self->{'__css'}      = undef;

    $self->{'__min_x'}    = 0;
    $self->{'__max_x'}    = 0;

    $self->{'__min_y'}    = 0;
    $self->{'__max_y'}    = 0;

    $self->{'__height'}   = 0;
    $self->{'__width'}    = 0;

    $self->{'__decimals'} = 0;
    $self->{'__scale'}    = 0;

    return bless $self, $pkg;
}

=head1 OBJECT METHODS

=cut

=head2 $obj->set_width($int)

I<Required>

=cut

sub set_width {
    my $self = shift;
    $self->{'__width'} = $_[0];
}

=head2 $obj->set_decimals($int)

I<Required>

=cut

sub set_decimals {
    my $self = shift;
    $self->{'__decimals'} = $_[0];
}

=head2 $obj->set_uri($str)

Set the URI used to identify the document in RDF metadata
section.

Default is '#'

=cut

sub set_uri {
    my $self  = shift;
    $self->{'__metadata'}->{'about'} = $_[0];
}

=head2 $obj->set_title($str)

Set the title for the document's RDF metadata section.

=cut

sub set_title {
    my $self  = shift;
    $self->{'__metadata'}->{'title'} = $_[0];
}

=head2 $obj->set_description($str)

Set the description for the document's RDF metadata section.

=cut

sub set_description {
    my $self  = shift;
    $self->{'__metadata'}->{'description'} = $_[0];
}

=head2 $obj->set_publisher($str)

Set the publisher for the document's RDF metadata section.

=cut

sub set_publisher {
    my $self = shift;
    $self->{'__metadata'}->{'publisher'} = $_[0];
}

=head2 $obj->set_language($str)

Set the language for the document's RDF metadata section.

=cut

sub set_language {
    my $self = shift;
    $self->{'__metadata'}->{'language'} = $_[0];
}

=head2 $obj->set_stylesheet($str)

Set the URI for the document's CSS stylesheet.

=cut

sub set_stylesheet {
    my $self = shift;
    $self->{'__css'} = $_[0];
}

=head2 $obj->add_point(\%args)

Points are added as SVG I<circle> elements.

Valid arguments are :

=over 4

=item * B<lat>

The latitude, in decimal form, of the point you are adding.

I<Required>

=item * B<long>

The longitude, in decimal form, of the point you are adding.

I<Required>

=item * B<id>

Default is 'id-<lat>-<long>', where decimal points are replaced
by '-'

=item * B<title>

A label for the point you are adding.

=item * B<radius>

The radius of the point you are adding.

Default is '1'

=item * B<style>

CSS stylings specific to the point you are adding.

=back 

=cut

sub add_point {
    my $self = shift;
    my $args = shift;

    if (ref($args) ne "HASH") {
	warn "arguments passed must be a hash reference";
	return 0;
    }

    if (! $args->{lat}) {
	warn "no latitude defined";
	return 0;
    }

    if (! $args->{long}) {
	warn "no longitude defined";
	return 0;
    }

    push @{$self->{'__points'}}, $args;
    return 1;
}

=head2 $obj->render($path)

Generate SAX2/SVG events for an ESRI shapefile.

=cut

sub render {
    my $self = shift;
    my $path = shift;

    my $shapefile = Geo::ShapeFile->new($path);

    if (! $shapefile) {

	return 0;
    }

    #

    ($self->{'__min_x'}, $self->{'__min_y'},
     $self->{'__max_x'}, $self->{'__max_y'}) = $shapefile->bounds();

    $self->{'__scale'}  = $self->{'__width'} / ($self->{'__max_x'} - $self->{'__min_x'});

    $self->{'__height'} = int((($self->{'__max_y'} - $self->{'__min_y'}) * 
			       $self->{'__scale'}) + 0.5);

    #
    
    $self->start_document();
    $self->xml_decl({Encoding=>"UTF-8",Version=>"1.0"});

    #

    if ($self->{'__css'}) {

	my $css = sprintf("href = \"%s\" type = \"text/css\"",
			  $self->{'__css'});

	$self->processing_instruction({Target => "xml-stylesheet",
				       Data   => $css});
    }

    #

    $self->start_prefix_mapping({Prefix       => "",
				 NamespaceURI => "http://www.w3.org/2000/svg"});

    $self->start_prefix_mapping({Prefix       => "xlink",
				 NamespaceURI => "http://www.w3.org/1999/xlink"});

    $self->start_prefix_mapping({Prefix       => "rdf",
				 NamespaceURI => "http://www.w3.org/1999/02/22-rdf-syntax-ns#"});

    $self->start_prefix_mapping({Prefix       => "dc",
				 NamespaceURI => "http://purl.org/dc/elements/1.1/"});
    
    $self->start_element({Name => "svg",
			  Attributes => { "{}height" => {Name  => "height",
							 Value => $self->{'__height'}},
					  "{}width"  => {Name  => "width",
							 Value => $self->{'__width'}}}});

    #

    $self->_metadata();

    #

    $self->start_element({Name => "g",
			  Attributes => {"{}id" => {Name  => "id",
						    Value => "map"}}});

    $self->start_element({Name => "rect",
			  Attributes => {"{}id"     => {Name  => "id",
							Value => "canvas"},
					 "{}height" => {Name  => "height",
							Value => $self->{'__height'}},
					 "{}width"  => {Name  => "width",
							Value => $self->{'__width'}},
				     }});
    
    $self->end_element({Name => "rect"});

    for (1 .. $shapefile->shapes()) {
	my $shape = $shapefile->get_shp_record($_);

	for(1 .. $shape->num_parts) {

	    my @points = $shape->get_segments($_);
	    my @d      = ();
	    
	    for my $i ( 0 .. $#points ) {

		# TO DO : pseudohashes are deprecated
		foreach my $xy ( keys %{$points[$i]} ) {
		    
		    # TO DO : argument $xy (e.g. "Y")
		    # isn't numeric element (see above
		    # re: pseudohashes)

		    my $coord = $points[$i][$xy]->$xy();

		    if ($xy eq "X"){
			$coord = $self->calc_x($coord);
		    } else {
			$coord = $self->calc_y($coord);
		    }
			
		    push @d, $coord;

		} 
	    }

	    $self->start_element({Name       => "path",
				  Attributes => {"{}d" => {Name => "d",
							   Value => join(" ","M",@d,"z")},
					     }});
	    $self->end_element({Name => "path"});
	}
    }

    $self->end_element({Name => "g"});

    #

    $self->_locations();

    #

    $self->end_element({Name => "svg"});

    $self->end_prefix_mapping({Prefix => ""});
    $self->end_prefix_mapping({Prefix => "rdf"});
    $self->end_prefix_mapping({Prefix => "xlink"});
    $self->end_prefix_mapping({Prefix => "dc"});

    $self->end_document();
    return 1;
}

sub _metadata {
    my $self = shift;

    my $data = $self->{'__metadata'};

    $self->start_element({Name => "metadata"});
    $self->start_element({Name => "rdf:RDF"});

    $self->start_element({Name       => "rdf:Description",
			  Attributes => {"{}about" => {Name  => "rdf:about",
						       Value => ($data->{about} || "#")}}});

    foreach my $el ("title","description","publisher","language") {
	if (exists($data->{ $el })) {
	    $self->start_element({Name => "dc:$el"});
	    $self->characters({Data    => $data->{ $el }});
	    $self->end_element({Name   => "dc:$el"});
	}
    }

    $self->start_element({Name => "dc:date"});
    $self->characters({Data=>Date::Simple->new()->format("%Y-%m-%d")});
    $self->end_element({Name => "dc:date"});

    $self->start_element({Name => "dc:format"});
    $self->characters({Data    => "image/svg+xml"});
    $self->end_element({Name   => "dc:format"});

    $self->end_element({Name => "rdf:Description"});
    $self->end_element({Name => "rdf:RDF"});
    $self->end_element({Name => "metadata"});

    return 1;
}

sub _locations {
    my $self = shift;

    if (! @{$self->{'__points'}}) {
	return 1;
    }

    $self->start_element({Name       => "g",
			  Attributes => { "{}id" => {Name  => "id",
						     Value => "locations"},}});

    map { 
	$self->_point($_);
    } @{$self->{'points'}};

    $self->end_element({Name => "g"});
    return 1;
}

sub _point {
    my $self = shift;
    my $args = shift;

    my %attrs = ("{}cx" => {Name  => "cx",
			    Value => $self->calc_x($args->{long})},
		 "{}cy" => {Name  => "cy",
			    Value => $self->calc_y($args->{lat})},
		 "{}r"  => {Name  => "r",
			    Value => ($args->{radius} || 1)});

    if ($args->{style}) {
	$attrs{ "{}style" } = {Name  => "style",
			       Value => $args->{style}};
    }

    #

    my $id = undef;

    if ($args->{'id'}) {
	$id = $args->{'id'};
    } 

    else {
	my $lat  = $args->{lat};
	my $long = $args->{long};

	$lat  =~ s/\./-/g;
	$long =~ s/\./-/g;

	$id = sprintf("id-%s-%s",$lat,$long);
    }

    #

    $self->start_element({Name       => "g",
			 Attributes => {"{}id" => {Name  => "id",
						   Value => $id}}});
    
    if ($args->{title}) {
	$self->start_element({Name => "title"});
	$self->characters({Data=>$args->{title}});
	$self->end_element({Name => "title"});
    }

    $self->start_element({Name      => "circle",
			 Attributes => \%attrs});
    $self->end_element({Name => "circle"});
    $self->end_element({Name => "g"});

    #

    return 1;
}

sub calc_x {
    my $self  = shift;
    my $coord = shift;
    
    return int(($coord - $self->{'__min_x'}) * $self->{'__scale'} *
	       (10**$self->{'__decimals'}))/ (10**$self->{'__decimals'});
}

sub calc_y {
    my $self  = shift;
    my $coord = shift;

    return int(($self->{'__max_y'} - $coord) * $self->{'__scale'} *
	       (10**$self->{'__decimals'}))/ (10**$self->{'__decimals'});
}

=head1 VERSION

0.2

=head1 DATE

$Date: 2004/08/21 04:13:28 $

=head1 AUTHOR

Aaron Straup Cope E<lt>ascope@cpan.orgE<gt>

=head1 SEE ALSO

http://www.webmapper.net/svg/create/ 

(these are the nice people who did most of the
 hard work for this package)

L<Geo::ShapeFile>

=head1 LICENSE

Copyright (c) 2004 Aaron Straup Cope. All rights reserved.

This is free software, you may use it and distribute it under the
same terms as Perl itself.

=cut

return 1;