scalable vector graphics

creating web maps in SVG from GIS data

unleash the data

Although the GIS community has started to adopt the Geography Markup Language (GML), another sibling of the XML family, to exchange geographic data, data is often stored in custom or proprietary formats as it makes manipulating and analysing the data using GIS software much easier. Fortunately, the data can usually be exported to a text file, e.g. the ESRI “ungenerate” format and MapInfo's MID/MIF format. This article describes how I managed to create the web map below using PERL to convert a text file to an SVG document.

ungenerate

The raw GIS data was available in ESRI “shape” format. Fortunately, every ArcView installation comes with a Avenue script to convert shape files to ungenerate files (shp2gen.ave). When running the Avenue script in ArcView, you specify:

  1. an output file name
  2. whether the features are points, lines, or polygons (I selected polygons, of course.)
  3. the field from which to create IDs in the generate file (I selected the field that contains the name of the province.)

When creating polygon files, the AUTO keyword is used along with the feature ID to indicate that labels will be automatically created when coverages are imported using ARC/INFO Generate.

from text to map

One of the stong points of PERL is its ability to manipulate text files quickly and efficiently. The script runs from the command line as follows:

perl gen2svg.pl input-file width [decimals]

The input file is used to specify the text file you wish to convert to SVG. The width determines the width in (pixels) of the SVG graphic. The decimals is an optional parameter to set the accuracy; the number of digits to keep after the decimal point in the coordinate values.

#!/usr/bin/perl

open INFILE, $ARGV[0] or die(“cannot open file”);

$width= $ARGV[1];
$n_decimals = $ARGV[2];

$min_x = 1.0e100;
$min_y = 1.0e100;
$max_x = -1.0e100;
$max_y = -1.0e100;

$data = 0;
$first = 0;
$c =1;

undef @polygon;

First of all, the polygons are extracted from the text file, coordinate by coordinate and stored in an array. The array of the first polygon of each province (some provinces have multiple polygons) also stores the name of the province. Iterating through the text file, the script tracks the minumum and maximum values of the x and y coordinates.

while (<INFILE>) {
	if ($data == 0) {
		if ($first != 0) {
			<INFILE>
			$first = 1;
		}
		$id = $_;
		chomp($id);
		$xy = <INFILE>;
	     	chomp ($xy);
		($x,$y) = split(/,\s+/,$xy);
		if (($id !~ /END/) && ($id !~ /\d*\.\d*/)){
			push (@polygon, $id);
			push @polygon, $x, $y;
		} else {
			$xy = $id;
			chomp ($xy);
			($x,$y) = split(/,\s+/,$xy);
			if ($id !~ /END/) {
				if ($x < $min_x){$min_x = $x;}
				if ($x > $max_x){$max_x = $x;}
				if ($y < $min_y){$min_y = $y;}
				if ($y > $max_y){$max_y = $y;}
				push @polygon, $x, $y;
			}
		}
		$data = 1;
	} else {
		$xy = $_;
		chomp ($xy);
		if ($xy =~ /END/){
			push (@polygon_list, [@polygon]);
			undef @polygon;
			$c++;
			$data = 0;
		} else {
			($x,$y) = split(/,\s+/,$xy);
			if ($x < $min_x){$min_x = $x;}
			if ($x > $max_x){$max_x = $x;}
			if ($y < $min_y){$min_y = $y;}
			if ($y > $max_y){$max_y = $y;}
			push @polygon, $x, $y;
		}  	
	}
}
close INFILE;

In the next step, these values are used to determine the height of the SVG graphic, related to the width as specified upon invoking the script.

$scale = $width / ($max_x - $min_x);
$height = ($max_y-$min_y) * $scale;
$height = int($height + 0.5);
print “<svg width=\“$width\” height=\“$height\”>”;

In the final step, the script creates a new group (<g>) in the SVG graphic for each province and then creates the associated polygons. As some of us are hesitant to release geometric information, the coordinates are defined using a local coordinate system. Based on the decimal parameter, the script manipulates the number of digits after the decimal point. Finally, each polygon is styled for them to be visible in the SVG graphic.

foreach $poly (@polygon_list) {
	$n = 0;
	$check = shift @$poly;
	if ($check =~ /\d*\.\d*/) {
		unshift (@$poly,$check);
	} else {
		print “</g>\n<g id=\“$check\”>\n”;
	}
	print “<path d=\“M”;
	foreach $coord (@$poly) {
		if ($n % 2 == 0){
			$coord = ($coord - $min_x) * $scale;
			$x = $coord;
			if ($n_decimals != 0) {
				$x = int($x * (10**$n_decimals))/
					(10**$n_decimals);
			}
			print “$x ”;
		} else {
			$coord = ($max_y - $coord) * $scale;
			$y = $coord;
			if ($n_decimals != 0) {
				$y = int($y * (10**$n_decimals))/
					(10**$n_decimals);
			}
			print “$y ”;
		}
	$n++;
	}
	print “\” stroke=\“red\” fill=\“yellow\” />\n”;
}
print “</svg>”;

Collating all the code snippets together, one can easily generate an SVG graphic from GIS data. So far it has worked for me. Please let me know if you think this code was useful for you, or whether you weren't able to make it work for your data. Good luck!

update: from ESRI shape to SVG

Geo::ShapeFile

When your data comes in ESRI shape files, the procedure described above asumes you have an ArcView installation so you can run an Avenue script to create an “ungenerate” file. However, there's a way to go straight from ESRI shape files to SVG without having to create an intermediate text file: the Geo::ShapeFile PERL module.

The Geo::ShapeFile module reads ESRI shape files containing GIS mapping data, it has support for shp (shape), shx (shape index), and dbf (data base) formats.

one loop to rule them all

The Geo::ShapeFile PERL module takes away a lot of the programming so you can focus on the issues that are important for the project you are working on. Once installed, you simply call the module at the start of your script:

#!/usr/bin/perl
use Geo::ShapeFile;

Now you can access the geometry and attributes of geographic data stored in the shape files. You run the script similar to the gen2svg.pl script:

perl shp2svg.pl input-file width [decimals]

Although the Geo::ShapeFile PERL module comes with a method width(), I found this resulted in unexpected values. Hence, I simply stuck to calculating the geographic width using $max_x -$min_x.

my $shapefile = new Geo::ShapeFile( $ARGV[0]);
my ($min_x, $min_y , $max_x , $max_y) = $shapefile->bounds();
my $width= $ARGV[1];
$n_decimals = $ARGV[2];
my $scale = $width/($max_x -$min_x);
my $graphic_height =  int(($max_y-$min_y) * $scale *
	(10**$n_decimals))/(10**$n_decimals);

for(1 .. $shapefile->shapes()) {
	my $shape = $shapefile->get_shp_record($_);
	for(1 .. $shape->num_parts) {
		print "<path d=\"M";
		my @points = $shape->get_segments($_);
		for my $i ( 0 .. $#points ) {
			foreach $xy ( keys %{$points[$i]} ) {
				my $coord=$points[$i][$xy]->$xy();
				if ($xy eq "X"){
					$coord = int(($coord - $min_x) * $scale *
						(10**$n_decimals))/
						(10**$n_decimals);
				} else {
					$coord = int(($max_y - $coord) * $scale *
						(10**$n_decimals))/
						(10**$n_decimals);
				}
			print "$coord ";
			} 
		}
		print "z\" />\n";
	}
}

Since you can access the attributes using the method get_dbf_record(), the script can be further enhanced to first group the paths together that belong to the same province and then put them within the same <g> tag.