# $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 =cut sub set_width { my $self = shift; $self->{'__width'} = $_[0]; } =head2 $obj->set_decimals($int) I =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 elements. Valid arguments are : =over 4 =item * B The latitude, in decimal form, of the point you are adding. I =item * B The longitude, in decimal form, of the point you are adding. I =item * B Default is 'id--', where decimal points are replaced by '-' =item * B 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;