#!/usr/bin/perl -w

#ogis2svg.pl - a conversion utility for the conversion of ESRI shapefiles to SVG
#Copyright (C) 2002-2007  Andreas Neumann
#
#This library is free software; you can redistribute it and/or
#modify it under the terms of the GNU Lesser General Public
#License as published by the Free Software Foundation; either
#version 2.1 of the License, or (at your option) any later version.
#
#This library is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#Lesser General Public License for more details.
#
#You should have received a copy of the GNU Lesser General Public
#License along with this library; if not, write to the Free Software
#Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#http://www.gnu.org/copyleft/lesser.html

use strict;
use Math::Round qw(:all);
use Getopt::Long;

my ($version, $program, $date);

$version = "0.5.1";
$date = "2007-05-08";
$program = $0;
$program =~ s/(\.\/)//;

#version history:
#version 0.1 initial version (2004-10)
#
#version 0.2 (2005-03-02)
#- added utf8 support for output, foreign characters should now work without problems
#- removed -db support, with the Postgis AsSVG() function this functionality is not needed anymore
#- added better rounding support: instead of nrdigits you now added a roundval: e.g. 0.001, 1 or 100
#- added reverse alphabetical ordering of groups
#- improved file reading to also support newer versions of the shp2pgsql program
#- added DTD extension for correct XML validation for elements with custom namespace attributes
#- cleaned up code
#- added a windows binary as an alternative to the pl script
#
#version 0.2.1 (2005-03-08)
#- corrected a bug with DTD extension
#
#version 0.3 (2005-04-19)
#- added support for point features represented as circle or symbol elements
#- added function makeCorrectId() to make ids conforming to XML ids, this function replaces blanks with underscores and special-characters
#
#version 0.4 (2005-06-21)
#- added command call to shp2pgsql to make conversion process quicker/easier (shp2pgsql needs to be available in a directory within the path-variable!)
#- added support for map scales to enable import into Adobe Illustrator
#- added support for units and unit conversions
#- added xmlns for xlink (for symbols)

#version 0.5 (2005-09-13)
#- added a '--ads' optional flag for setting defaults for the ADS (Atlas of Switzerland) team; watch out for $ads variables regarding these modifications
#- corrected a rounding bug for '"M"oveto-coordinates' when dealing with scale conversions
#- corrected a replacing bug in MULTILINESTRINGs where the character 'z' appeared in attribute values
#- added LGPL license

#version 0.5.1 (2007-05-08)
#- added a '--viewbox=x y width height' option, values are used if specified instead of the calculated min/max values
#- corrected --referenceframe option in case no scale was given
#- corrected grouping behaviour if only one attribute was selected
#- generated new executable for Windows which doesn't expire

my ($sqlfile,$svgfile,$shpfile,$status);
my ($line,$selectline,$geometry,$newElement,$xOffset,$yOffset,$lindex);
my (@myPolys,@myCoords,@coords,@interiorGeometry,@polys,@array);
my ($x_before,$y_before);
my ($polyCount,$counter,$nrValues,$key,$value,$id,$testAttrib,$handler,$groupAttribInclude);
my ($x,$y,$j,$mypoly,$poly,$array);
my $pcount = 0;
my ($interiorPoly,$roundVal,$type,$dummy,$scale,$inputunits,$outputunits,$referenceframe,$viewBox,$ads,$adsnameIndex);
my ($layername,$tablename);
my (@tempArray,@svgGeomArray);
my (@attribNames,@attribTypes,@attribUse,@attribData);
my ($attribLine,$element,$geomIndex,$endIndex,$mySvgString);
my ($minXCoor,$maxXCoor,$minYCoor,$maxYCoor);
my ($attribName,$uniqueAttrib);
my %uniqueGroupData; #this is to hold unique attribs for group Attribs
my $uniqueGroupData;
my %uniqueSymbols; #this is to hold unique symbol names
my $uniqueSymbols;
my %styles; #this hash holds style values matching the keys
my ($styles,$style);
my (@myKeys,@handlerArray,@viewBoxValues,$Xwidth,$Ywidth);
my ($brackIndex,$strLength,);
my ($red,$green,$blue);
my $usage = "$program (version $version, $date)\nUsage: $program --input yourinputShapeFile --output youroutput.svg --roundval 0.1 [--scale 25000] [--inputunits m] [--outputunits mm] [--referenceframe] [--viewBox '480000 -300000 350000 220000'] [--ads]\n";

#initialize random generator
srand();

#get parameters
GetOptions("input=s" => \$shpfile,
		"output=s" => \$svgfile,
		"roundval=f" => \$roundVal,
		"scale=f" => \$scale,
		"inputunits=s" => \$inputunits,
		"outputunits=s" => \$outputunits,
		"referenceframe" => \$referenceframe,
		"viewBox=s" => \$viewBox,
		"ads" => \$ads);

unless ($shpfile) {
	die "$usage you have to specify an input file (shp)!\n";
}

unless ($svgfile) {
	die "$usage you have to specify an output file (svg or sql)!\n";
}

unless ($roundVal) {
	die "$usage you have to specify the roundval!\n";
}

#this part is for the Atlas of Switzerland (default values)
if ($ads) {
	if (!$scale) {
		$scale = 500000;	
	}
	if (!$inputunits) {
		$inputunits = "m";
	}
	if (!$outputunits) {
		$outputunits = "mm";
	}
	if (!$referenceframe) {
		$referenceframe = 1;	
	}
}

#check scale and input/output unit conversions and modify scale-factor in case of unit conversions
if (!$scale) {
	$scale = 1;
}
else {
	if ($inputunits && $outputunits) {
		if ($inputunits ne $outputunits) {
			if ($inputunits eq "m") {
				if ($outputunits eq "mm") {
					$scale /= 1000;
				}
				elsif ($outputunits eq "cm") {
					$scale /= 100;
				}
				elsif ($outputunits eq "dm") {
					$scale /= 10;
				}
				else {
					die "unknown output units - currently we only support 'mm', 'cm', 'dm' and 'm' as input units!\n";
				}
			}
			else {
				die "unknown input units - currently we only support 'm' as input units!\n";
			}
		}
	}
}

#see if viewBox was specified and contains 4 values
if ($viewBox) {
	@viewBoxValues = split(/\s+/,$viewBox);
	if ($#viewBoxValues != 3) { #zero based!
		die "Error: --viewBox has to contain 4 values, e.g. --viewBox='700000 -221000 21000 11000'\n";
	}
}

($layername,$dummy) = split(/\./,$svgfile);
print "\nworking on layer $layername ...\n";

$sqlfile = $layername.".sql";

#first convert the shapefile to a temporary sqlfile
print "converting shapefile to a temporary sqlfile ...";
$status = system("shp2pgsql ".$shpfile." ".$layername." >".$sqlfile);

if ($status != 256) {
	if ($status == 65280) {
		die "could not find shapefile or some of its components!\n\n";
	}
	elsif ($status == 32512) {
		die "could not find executable shp2pgsl. Please copy it to a directory available in your path!\n\n";
	}
	else {
		die "unknown error with conversion from shape to temporary sql file!\n\n";
	}
}
print " done.\n\n";

#open sql-file
open(SQLFILE, "<$sqlfile") || die "could not open SQLFILE $sqlfile for reading!";
while (<SQLFILE>) {
	$line = $_;
	if ($line =~ /^CREATE/i) {
		($dummy,$dummy,$tablename,$dummy) = split(/\s/,$line);
		$brackIndex = index($line,"(");
		$strLength = length($line);
		$tablename =~ s/"//g;
		print "tablename: $tablename\n\n";
		$line = substr($line,$brackIndex+1,$strLength - ($brackIndex+4));
		(@tempArray) = split(/\s*,\s+/,$line);
		last;
	}
}

#ask which attributes the user wants to include
if (!$ads) {
	print "The following attributes are available. Please select the attributes you want to include in the SVG export:\n\n";
}
my $i = 0;
my @selectedAttribs;
my $adsCounter = 0;
$adsnameIndex = -1;
my $uniqueAttribIndex = -1;
foreach $element (@tempArray) {
	($attribNames[$i],$attribTypes[$i]) = split(/\s/,$element);
	$attribNames[$i] =~ s/"//g;
	if ($ads) {
		#test if attributes "id" and "name" are available
		if ($attribNames[$i] eq "id" || $attribNames[$i] eq "name") {
			push(@selectedAttribs,$attribNames[$i]);
			$adsCounter++;
			$attribUse[$i] = "y";
			if ($attribNames[$i] eq "id") {
				$uniqueAttribIndex = $i;
				$uniqueAttrib = "id";
			}
			if ($attribNames[$i] eq "name") {
				$adsnameIndex = $i;	
			}
		}
		else {
			$attribUse[$i] = "n";
		}
	}
	else {
		print "Attribute=$attribNames[$i], Type=$attribTypes[$i]; Do you want to include it [y|n]?";
		$attribUse[$i] = "";
		while ($attribUse[$i] ne "y" && $attribUse[$i] ne "n") {
			$attribUse[$i] = <stdin>;
			chomp($attribUse[$i]);
			if ($attribUse[$i] ne "y" && $attribUse[$i] ne "n") {
				print "Please answer 'y' or 'n'!\n";
				print "Attribute=$attribNames[$i], Type=$attribTypes[$i]; Do you want to include it [y|n]?";
			}
			if ($attribUse[$i] eq "y") {
				push(@selectedAttribs,$attribNames[$i]);
			}
		}
	}
	$i++;
}
if ($ads) {
	#stop execution if id and name aren't present
	die "your shapefile does not contain the compulsory attributes 'id' and 'name'!\nStopped script execution ...\n" if $adsCounter < 1;	
}

#ask which attribute should be unique id?, ask if one wants to group and which attribute
my $selectedAttribsString = join(", ",@selectedAttribs);
my $group = "";
my $groupAttribIndex = -1;
my $groupAttrib = "";
my $sortMethod = "";
my $styleFile = "";
while ($uniqueAttribIndex == -1) {
	print "\nYou selected the following Attributes: $selectedAttribsString\n";
	print "Which one would you like to select as a unique svg-id?\n";
	print "Type in attribute Name or 'none' if you don't want to include a unique id:\n";
	$uniqueAttrib = <stdin>;
	chomp($uniqueAttrib);
	$counter = 0;
	foreach $attribName (@attribNames) {
		if (($attribName eq $uniqueAttrib) and ($attribUse[$counter] eq "y")) {
			$uniqueAttribIndex = $counter;
			$attribUse[$counter] = "";
			$selectedAttribsString =~ s/$uniqueAttrib(\,\s)*//;
		}
		$counter++;
	}
	if ($uniqueAttrib eq "none") {
		$uniqueAttribIndex = -2;
	}
	if ($uniqueAttribIndex == -1) {
		print "your attribute does not match existing attributes!\n";
	}
}
print "\n\n you selected \"$uniqueAttrib\" as a unique attribute ...\n\n";

#ask which attribute to group?
if ($ads) {
	$group = "n";
}
else {
	print "\nDo you want to group the data according to one attribute? (type 'y' or 'n')\n";	
}
my $selAttrLengths = @selectedAttribs;
if ($selAttrLengths > 0) {
	while ($group ne "y" && $group ne "n") {
		$group = <stdin>;
		chomp($group);
		if ($group ne "y" && $group ne "n") {
			print "\nPlease answer 'y' or 'n'!\n";
			print "Do you want to group the data according to one attribute? (type 'y' or 'n')\n";
		}
	}
	if ($group eq "y") {
		while ($groupAttribIndex == -1) {
			print "\nPlease select one of the following Attributes for grouping: $selectedAttribsString\n";
			print "Type in attribute Name:\n";
			$groupAttrib = <stdin>;
			chomp($groupAttrib);
			$counter = 0;
			foreach $attribName (@attribNames) {
				if (($attribName eq $groupAttrib) and ($attribUse[$counter] eq "y")) {
					$groupAttribIndex = $counter;
				}
				$counter++;
			}
			if ($groupAttribIndex == -1) {
				print "your given attribute does not match existing attributes!\n";
			}
		}
		#ask if the user wants to include the group attrib as well
		$groupAttribInclude = "x";
		while ($groupAttribInclude ne "y" && $groupAttribInclude ne "n") {
			print "\nDo you want to include the group-Attrib ".$attribNames[$groupAttribIndex]." as an attribute?\n";
			print "Type 'y' or 'n'. 'n' indicates that you want to use the attribute only for grouping.:\n";
			$groupAttribInclude = <stdin>;
			chomp($groupAttribInclude);
		}
		if ($groupAttribInclude eq "n") {
			$attribUse[$groupAttribIndex] = "n";
		}
		print "\nDo you want to sort the group alphabetically, alphab. reversed, by nr ascending or by nr descending?\n";
		print "Type 'a', 'd', 'nra' or 'nrd':\n";
		while ($sortMethod ne "a" && $sortMethod ne "d" && $sortMethod ne "nra" && $sortMethod ne "nrd") {
			$sortMethod = <stdin>;
			chomp($sortMethod);
			if ($sortMethod ne "a" && $sortMethod ne "d" && $sortMethod ne "nra" && $sortMethod ne "nrd") {
				print "\nPlease answer 'a', 'd', 'nra' or 'nrd'!\n";
				print "\nDo you want to sort the group alphabetical, by nr ascending or by nr descencding?\n";
				print "Type 'a', 'd', 'nra' or 'nrd':\n";
			}
		}
		print "\n\n you selected \"$groupAttrib\" as a group attribute, sorted by method '$sortMethod' ...\n\n";
		print "Do you want to enter a style-file containing group/style matches?\n";
		print "Enter filename (incl. extension) or just press enter if you don't have one:\n";
		$styleFile = <stdin>;
		chomp($styleFile);
	}
}

my $useHandler = "";
if ($ads) {
	$useHandler = "n";
}
while ($useHandler ne "y" && $useHandler ne "n") {
	print "\nWould you like to include event-handlers to the individual elements? Type (y|n)\n";
	$useHandler = <stdin>;
	chomp($useHandler);
	if ($useHandler ne "y" && $useHandler ne "n") {
		print "You have to enter \"y\" or \"n\" to answer this question!\n";
	}
}

if ($useHandler eq "y") {
	print "\nPlease enter the eventhandlers you want to include, separated by commas, f.e. \"onclick,onmouseover,onmouseut\":\n";
	my $tempText = <stdin>;
	chomp($tempText);
	@handlerArray = split(/,/,$tempText);
}

#extracting geometry type
$selectline = <SQLFILE>;
$line = $selectline;
$line =~ s/select AddGeometryColumn\(//;
$line =~ s/\);//;
($dummy,$dummy,$dummy,$dummy,$type,$dummy) = split(/\'*,\'*/,$line);
print "\nGeometry type = $type\n";
die "Currently only POINT, MULTIPOLYGON and MULTILINESTRING allowed!" unless $type eq "MULTIPOLYGON" || $type eq "MULTILINESTRING" || $type eq "POINT";

#ask how to represent point geometry
my $pointRepresenter = "";
my $symbolColumn = -2; #-1 means no column, -2 means not yet defined
my $symbolGenerate = "";
my $columnName = "";
if ($type eq "POINT") {
	while ($pointRepresenter ne "circle" && $pointRepresenter ne "symbol" && $pointRepresenter ne "rect") {
		print "\nHow would you like to represent the POINT elements? Type (circle|symbol)\n";
		$pointRepresenter = <stdin>;
		chomp($pointRepresenter);
		if ($pointRepresenter ne "circle" && $pointRepresenter ne "symbol") {
			print "You have to enter \"circle\" or \"symbol\" to answer this question!\n";
		}
	}
	if ($pointRepresenter eq "symbol") {
		while ($symbolColumn == -2) {
			print "\nDo you have a column indicating the symbol type?\n";
			print "Type \"none\" if you don't, or one of the following columns: $selectedAttribsString\n";
			print "Type in attribute Name:\n";
			$columnName = <stdin>;
			chomp($columnName);
			$counter = 0;
			foreach $attribName (@attribNames) {
				if (($attribName eq $columnName) and ($attribUse[$counter] eq "y")) {
					$symbolColumn = $counter;
				}
				$counter++;
			}
			if ($columnName eq "none") {
				$symbolColumn = -1; #case no column for symbol types
			}
			if ($symbolColumn == -2) {
				print "your given attribute does not match existing attributes!\n";
			}
		}
		while ($symbolGenerate ne "y" && $symbolGenerate ne "n") {
			print "\nDo you want the program to automatically generate symbol definitions for you? Type (y|n)\n";
			$symbolGenerate = <stdin>;
			chomp($symbolGenerate);
			if ($symbolGenerate ne "y" && $symbolGenerate ne "n") {
				print "You have to enter \"y\" or \"n\" to answer this question!\n";
			}
		}
	}
}

while(<SQLFILE>) {
	$line = $_;
	if (($line =~ /^insert/i) && ($line =~ /GeometryFromText/)) {
		#extract attributes
		$line =~ /values\s*\(/gi;
		$brackIndex = pos($line);
		$line =~ /GeometryFromText\s*\(/gi;
		$geomIndex = pos($line) - length($&); #$& contains the text that matched
		$attribLine = substr($line,$brackIndex,$geomIndex - $brackIndex);
		chop($attribLine); #remove last Komma
		$attribLine =~ s/&/&amp;/g;
		$attribLine =~ s/NULL/'NULL'/g;
		(@attribData) = split(/','/,$attribLine);
		$i = 0;
		foreach (@attribData) {
			$attribData[$i] =~ s/'//g;
			$attribData[$i] = replaceSpecialChars($attribData[$i]);
			$i++;
		}

		#start svg-string with id or no id
		if ($type eq "POINT") {
			if ($pointRepresenter eq "circle") {
				$mySvgString = "<circle ";
			}
			if ($pointRepresenter eq "symbol") {
				$mySvgString = "<use ";
			}
		}
		else {
			$mySvgString = "<path ";
		}
		if ($uniqueAttrib ne "none") {
			if ($ads) {
				if ($adsnameIndex == -1) {
					$id = makeCorrectId("ID_x5F_".$attribData[$uniqueAttribIndex]);					
				}
				else {
					$id = makeCorrectId("ID_x5F_".$attribData[$uniqueAttribIndex]."_".$attribData[$adsnameIndex]);
				}
			}
			else {
				$id = makeCorrectId($tablename."_".$attribData[$uniqueAttribIndex]);
			}
			$mySvgString .= "id=\"".$id."\" ";
		}
		#add event handler, if requested
		if ($useHandler eq "y") {
			foreach $handler (@handlerArray) {
				$mySvgString .= $handler."=\"showData(evt);\" ";
			}
		}

		#output of attribute data
		if (!$ads) {
			for ($i=0;$i<scalar(@attribData);$i++) {
				if ($attribUse[$i] eq "y") {
					$mySvgString = $mySvgString."attrib:".$attribNames[$i]."=\"".$attribData[$i]."\" ";
				}
			}
		}

		#extract geometry
		$geometry = substr($line,$geomIndex+18);
		$endIndex = index($geometry,"',");
		$geometry = substr($geometry,0,$endIndex);

		#substitute unneccessary info, begin and end of open-gis linestring
		if ($type eq "MULTIPOLYGON") {
			$geometry =~ s/(MULTIPOLYGON\s*\(\(\()//g;
			$geometry =~ s/(\)\)\))//g;
		}
		elsif ($type eq "MULTILINESTRING") {
			$geometry =~ s/(MULTILINESTRING\s*\(\()//g;
			$geometry =~ s/(\)\))//g;
		}
		elsif ($type eq "POINT") {
			$geometry =~ s/(POINT\s*\()//g;
			$geometry =~ s/(\))//g;
		}
		if ($type eq "MULTILINESTRING" || $type eq "MULTIPOLYGON") {
			undef @myPolys;
			#splits into an array that can hold multiple polygons
			@myPolys = split(/\)\)\,\(\(/,$geometry);
			$polyCount = 0;
			foreach $mypoly (@myPolys) {
				#split into an array that can hold geometry with holes (interior rings)
				@interiorGeometry = split(/\)\,\(/,$mypoly);
				foreach $interiorPoly (@interiorGeometry) {
					undef @myCoords;
					@myCoords = split(/,/,$interiorPoly);
					undef @coords;
					@coords = split(/\s+/,$myCoords[0]);
					$x_before = $coords[0] / $scale;
					$y_before = $coords[1] / $scale;
					#initialize min/max comparison
					if ($pcount == 0 && $polyCount == 0) {
						$minXCoor = $x_before;
						$maxXCoor = $x_before;
						$minYCoor = $y_before;
						$maxYCoor = $y_before;
					}
					if ($polyCount == 0) {
						$mySvgString = $mySvgString."d=\"M".nearest($roundVal,$x_before)." ".nearest($roundVal,($y_before * -1));
					}
					else {
						if ($type eq "MULTIPOLYGON") {
							$mySvgString = $mySvgString."zM".nearest($roundVal,$x_before)." ".nearest($roundVal,($y_before * -1));
						}
						if ($type eq "MULTILINESTRING") {
							$mySvgString = $mySvgString."M".nearest($roundVal,$x_before)." ".nearest($roundVal,($y_before * -1));
						}						
					}
					for ($j=1;$j<scalar(@myCoords);$j++) {
						@coords = split(/\s+/,$myCoords[$j]);
						$x = nearest($roundVal,($coords[0] / $scale) - $x_before);
						$y = nearest($roundVal,(($coords[1] / $scale) - $y_before) * -1);
						$x_before = $coords[0] / $scale;
						$y_before = $coords[1] / $scale;
						$x =~ s/^(0\.)/\./;
						$x =~ s/^(-0\.)/-\./;
						$y =~ s/^(0\.)/\./;
						$y =~ s/^(-0\.)/-\./;
						$mySvgString = $mySvgString."l".$x." ".$y;
						#compare for getting min-max values
						if ($x_before < $minXCoor) {
							$minXCoor = $x_before;
						}
						if ($x_before > $maxXCoor) {
							$maxXCoor = $x_before;
						}
						if ($y_before < $minYCoor) {
							$minYCoor = $y_before;
						}
						if ($y_before > $maxYCoor) {
							$maxYCoor = $y_before;
						}
					}
					$polyCount++;
				}
			}
		}
		elsif ($type eq "POINT") {
			undef @coords;
			@coords = split(/\s+/,$geometry);
			$coords[0] /= $scale;
			$coords[1] /= $scale;
			if ($pcount == 0) {
				$minXCoor = $coords[0];
				$maxXCoor = $coords[0];
				$minYCoor = $coords[1];
				$maxYCoor = $coords[1];
			}
			if ($coords[0] < $minXCoor) {
				$minXCoor = $coords[0];
			}
			if ($coords[0] > $maxXCoor) {
				$maxXCoor = $coords[0];
			}
			if ($coords[1] < $minYCoor) {
				$minYCoor = $coords[1];
			}
			if ($coords[1] > $maxYCoor) {
				$maxYCoor = $coords[1];
			}
			if ($pointRepresenter eq "circle") {
				$mySvgString .= "cx=\"".nearest($roundVal,$coords[0])."\" cy=\"".(nearest($roundVal,$coords[1])*-1)."\" r=\"1";
			}
			if ($pointRepresenter eq "symbol") {
				if ($symbolColumn == -1) {
					$mySvgString .= "xlink:href=\"#symbolNone\" ";
				}
				else {
					$attribData[$symbolColumn] = makeCorrectId($attribData[$symbolColumn]);
					$mySvgString .= "xlink:href=\"#".$attribData[$symbolColumn]."\" ";
				}
				$mySvgString .= "transform=\"translate(".nearest($roundVal,$coords[0])." ".(nearest($roundVal,$coords[1])*-1)."),scale(1)";
			}
		}
		if ($type eq "MULTIPOLYGON") {
			$mySvgString = $mySvgString."z";
		}
		if ($group eq "y") {
			#determine unique attributes
			$testAttrib = $attribData[$groupAttribIndex];
			push(@{ $uniqueGroupData{$testAttrib} }, $mySvgString."\" />");
		}
		if ($type eq "POINT" && $symbolGenerate eq "y" && $symbolColumn != -1) {
			#determine unique symbol values
			if (!exists($uniqueSymbols{$attribData[$symbolColumn]})) {
				$red = int(rand(255));
				$green = int(rand(255));
				$blue = int(rand(255));
				$uniqueSymbols{$attribData[$symbolColumn]} = "fill=\"rgb(".$red.",".$green.",".$blue.")\"";
			}
		}
		push (@svgGeomArray,$mySvgString."\" />");
		$pcount++;
		if ($pcount % 500 == 0) {
			print "converted $pcount objects ...\n";
		}
	}
}

close(SQLFILE);

print "Converted $pcount $type objects - Done.\n";

if ($viewBox) {
	print "xmin: $viewBoxValues[0], xmax: ".($viewBoxValues[0]+$viewBoxValues[2]).", ymin: ".(($viewBoxValues[3]+$viewBoxValues[1])*-1).", ymax: ".($viewBoxValues[1]*-1)."\n";

	#use viewBox values for min/max
	$minXCoor = $viewBoxValues[0] / $scale;
	$maxXCoor = $minXCoor + $viewBoxValues[2] / $scale;
	$maxYCoor = $viewBoxValues[1] / $scale;
	$minYCoor = $maxYCoor + $viewBoxValues[3] / $scale;
	$Xwidth = $viewBoxValues[2] / $scale;
	$Ywidth = $viewBoxValues[3] / $scale;
}
else {
	#calculate min-max
	$minXCoor = nearest($roundVal,$minXCoor);
	$maxXCoor = nearest($roundVal,$maxXCoor);
	$minYCoor = nearest($roundVal,$minYCoor);
	$maxYCoor = nearest($roundVal,$maxYCoor);
	$Xwidth = $maxXCoor - $minXCoor;
	$Ywidth = $maxYCoor - $minYCoor;

	print "xmin: $minXCoor, xmax: $maxXCoor, ymin: $minYCoor, ymax: $maxYCoor\n";

	#add margin around min/max (2.5% each side)
	$minXCoor = nearest($roundVal,$minXCoor - $Xwidth * 0.025);
	$maxYCoor = nearest($roundVal,($maxYCoor + $Ywidth * 0.025) * -1);
	$Xwidth = nearest($roundVal,$Xwidth + $Xwidth * 0.05);
	$Ywidth = nearest($roundVal,$Ywidth + $Ywidth * 0.05);
}

print "writing SVG file ...\n";

#open svg-file
open SVGFILE, ">:utf8", "$svgfile" || die "could not open SVGFILE $svgfile for writing!";

print SVGFILE "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
print SVGFILE "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\" [\n";
#extend the DOCTYPE
print SVGFILE "<!ATTLIST svg\n\txmlns:attrib CDATA #IMPLIED\n>\n";
#extend path element attributes if user selected any attributes
my $attribWritten = 0;
for ($i=0;$i<scalar(@attribData);$i++) {
	if ($attribUse[$i] eq "y") {
		if ($attribWritten == 0) {
			if ($type eq "MULTIPOLYGON" || $type eq "MULTILINESTRING") {
				print SVGFILE "<!ATTLIST path\n";
			}
			else {
				if ($pointRepresenter eq "circle") {
					print SVGFILE "<!ATTLIST circle\n";
				}
				elsif ($pointRepresenter eq "symbol") {
					print SVGFILE "<!ATTLIST use\n";
				}
			}
		}
		print SVGFILE "\tattrib:".$attribNames[$i]." CDATA #IMPLIED\n";
		$attribWritten++;
	}
}
if ($attribWritten > 0) {
	print SVGFILE ">\n";
}
if ($referenceframe && !$ads) {
	print SVGFILE "<!ATTLIST rect\n";
	print SVGFILE "\tattrib:xmin CDATA #IMPLIED\n";
	print SVGFILE "\tattrib:xmax CDATA #IMPLIED\n";
	print SVGFILE "\tattrib:ymin CDATA #IMPLIED\n";
	print SVGFILE "\tattrib:ymax CDATA #IMPLIED\n";
	print SVGFILE ">\n";
}
print SVGFILE "]>\n";

if ($outputunits) {
	print SVGFILE "<svg width=\"".$Xwidth.$outputunits."\" height=\"".$Ywidth.$outputunits."\" ";
	if ($ads) {
		$xOffset = (0 - 480000) / $scale;
		$yOffset = (0 - 302000) / $scale * -1;
	}
	else {
		$xOffset = 0 - nearest($roundVal,$minXCoor);
		$yOffset = 0 - nearest($roundVal,$maxYCoor);
	}
	print SVGFILE "viewBox=\"0 0 $Xwidth $Ywidth\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:attrib=\"http://www.carto.net/attrib/\">\n";
}
else {
	print SVGFILE "<svg width=\"100%\" height=\"100%\" ";
	if ($viewBox) {
		print SVGFILE "viewBox=\"$viewBox\" ";
	}
	else {
		print SVGFILE "viewBox=\"$minXCoor $maxYCoor $Xwidth $Ywidth\" ";
	}
	print SVGFILE "xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:attrib=\"http://www.carto.net/attrib/\">\n";
}
if ($useHandler eq "y") {
	print SVGFILE "\t<script type=\"text/ecmascript\"> <![CDATA[\n";
	print SVGFILE "\t\tfunction showData(evt) {\n";
	print SVGFILE "\t\t\t//do something here ...\n";
	print SVGFILE "\t\t}\n";
	print SVGFILE "\t]]> </script>\n";
}
my $radius = "r=\"".nearest($roundVal,$Xwidth * 0.0015)."\"";
if ($type eq "POINT") {
	if ($symbolGenerate eq "y") {
		print SVGFILE "\t<defs>\n";
		if ($symbolColumn == -1) {
			print SVGFILE "\t\t<symbol id=\"symbolNone\" overflow=\"visible\">\n";
			print SVGFILE "\t\t\t<circle cx=\"0\" cy=\"0\" fill=\"red\" ".$radius."/>\n";
			print SVGFILE "\t\t</symbol>\n";		}
		else {
			while (($key,$value) = each(%uniqueSymbols)) {
				print SVGFILE "\t\t<symbol id=\"".$key."\" overflow=\"visible\">\n";
				print SVGFILE "\t\t\t<circle cx=\"0\" cy=\"0\" ".$value." ".$radius."/>\n";
				print SVGFILE "\t\t</symbol>\n";
			}
		}
		print SVGFILE "\t</defs>\n";
	}
	writeReferenceFrame();
	print SVGFILE "\t<g id=\"$tablename\">\n";
}
else {
	writeReferenceFrame();
	print SVGFILE "\t<g id=\"$tablename\" fill=\"none\" stroke=\"black\" stroke-width=\"".nearest($roundVal,$Xwidth*0.0005)."\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n";
}
if ($group eq "y") {
	#open and read stylefile if any
	if (length($styleFile) > 0) {
		open(STYLEFILE, "<$styleFile") || die "could not open style file $styleFile for reading!";
		while(<STYLEFILE>) {
			$line = $_;
			($key,$style) = split(/\t/,$line);
			chomp($style);
			$styles{$key} = $style;
		}
		close(STYLEFILE);
	}

	#print out data and status report on group
	my $nrUniqueData = keys(%uniqueGroupData);
	print "\nGroup Summary: Our group attribute ($groupAttrib) has $nrUniqueData unique values.\n";
	#determine sorting
	if ($sortMethod eq "a" || $sortMethod eq "d") {
		@myKeys = sort(keys(%uniqueGroupData));
		if ($sortMethod eq "d") {
			@myKeys = reverse(@myKeys);
		}
	}
	elsif ($sortMethod eq "nra") {
		@myKeys = sort( {$a <=> $b} keys(%uniqueGroupData));
	}
	else {
		@myKeys = sort( {$b <=> $a} keys(%uniqueGroupData));
	}
	foreach $key (@myKeys) {
		$nrValues = @{$uniqueGroupData{$key}};
		$id = makeCorrectId($tablename."__".$key);
		print "Group $key has $nrValues elements\n";
		print SVGFILE "\t\t<g id=\"".$id."\"";
		print SVGFILE " style=\"".$styles{$key}."\"" if exists $styles{$key};
		print SVGFILE ">\n";
		my @records = @{$uniqueGroupData{$key}};
		foreach $element (@records) {
			$element = writeSingleElement($element);
			print SVGFILE "\t\t\t$element\n";
		}
		print SVGFILE "\t\t</g>\n";
	}
	print "\n";
}
else {
	foreach $element (@svgGeomArray) {
		$element = writeSingleElement($element);
		print SVGFILE "\t\t$element\n";
	}
}

print SVGFILE "\t</g>\n";

print SVGFILE "</svg>";

close(SVGFILE);

#removing temporary sqlfile
print "removing temporary sqlfile ...";
unlink($sqlfile);
print "done!\n\n";

print "done!\n\n";

#subroutine to replace special characters
sub replaceSpecialChars {
	my $textstring = shift;
	$textstring =~ s/&/&#038;/g;
	$textstring =~ s/"/&#34;/g;
	$textstring =~ s/</&#060;/g;
	$textstring =~ s/>/&#062;/g;
	return $textstring;
}

#subroutine to write out a single element (path or circle)
sub writeSingleElement {
	my $element = shift;
	if ($type eq "POINT") {
		if ($pointRepresenter eq "circle") {
			$element =~ s/(r="1")/$radius/gi;
			if ($outputunits) {
				my @array = split(/ cx="|" cy="|" r="/,$element);
				$element = $array[0]." cx=\"".($array[1] + $xOffset)."\" cy=\"".($array[2] + $yOffset)."\" ".$radius." />";
			}
		}
		if ($pointRepresenter eq "symbol" && $outputunits) {
			my @array = split(/translate\(|\),scale/,$element);
			my @coords = split(/\s/,$array[1]);
			$element = $array[0]."translate(".($coords[0] + $xOffset)." ".($coords[1] + $yOffset)."),scale".$array[2];
		}
	}
	if ($type eq "MULTIPOLYGON" || $type eq "MULTILINESTRING") {
		if ($outputunits) {
			#now recalculate moveto elements
			@array = split(/\s+d="/,$element);
			$element = $array[1];
			$element =~ s/" \/>//;
			$newElement = $array[0]." d=\"";
			@polys = split(/M/,$element);
			foreach $poly (@polys) {
				if (length($poly) > 0) {
					$lindex = index($poly,"l");
					my @coords = split(/l/,$poly);
					my @coordinates = split(/\s/,$coords[0]);
					$newElement .= "M".nearest($roundVal,($coordinates[0] + $xOffset))." ".nearest($roundVal,($coordinates[1] + $yOffset)).substr($poly,$lindex);
				}
			}
			$element = $newElement."\" />";
		}
	}
	return $element;
}

#subroutine to make a correct xml id by replacing blanks with underscores and removing other characters not allowed in a XML id
sub makeCorrectId {
	my $textstring = shift;
	#remove trailing whitespaces
	$textstring =~ s/\s*$//;
	#replace whitespaces with underscores
	$textstring =~ s/\s+/_/gi;
	#replace all characters that aren't characters, umlauts, french characters, numbers or underscore
	$textstring =~ s/[^a-zA-Z0-9_-äöüÄÖÜéàèáâêôÉÀÈÁÂÊÔ]//gi;
	#add a underscore if it starts with a number
	if ($textstring =~ /^[0-9]/) {
		$textstring = "_".$textstring;
	}
	return $textstring;
}

#subroutine to write reference frame
sub writeReferenceFrame {
	#add referenceframe
	if ($referenceframe) {
		if ($scale != 1) {
			if ($ads) {
				print SVGFILE "\t<g id=\"ID_x5F_-99\">\n";
				print SVGFILE "\t\t<rect id=\"ID_x5F_-99_N302000_E865000_S62000_W480000\" fill=\"none\" stroke=\"black\" stroke-width=\"".nearest($roundVal,$Xwidth*0.0005)."\" x=\"".nearest($roundVal,(480000 / $scale + $xOffset))."\" y=\"".nearest($roundVal,(302000 / $scale * -1 + $yOffset))."\" width=\"".nearest($roundVal,((865000 - 480000) / $scale))."\" height=\"".nearest($roundVal,((302000 - 62000) / $scale))."\" />\n";
				print SVGFILE "\t</g>\n";
			}
			else {
				print SVGFILE "\t<rect id=\"referenceFrame\" fill=\"none\" stroke=\"black\" stroke-width=\"".nearest($roundVal,$Xwidth*0.0005)."\" x=\"0\" y=\"0\" width=\"$Xwidth\" height=\"$Ywidth\" attrib:xmin=\"".($minXCoor * $scale)."\" attrib:xmax=\"".(($minXCoor + $Xwidth)*$scale)."\" attrib:ymin=\"".(($maxYCoor * -1 * $scale) - ($Ywidth * $scale))."\" attrib:ymax=\"".($maxYCoor * -1 * $scale)."\" />\n";
			}
		}
		else {
				print SVGFILE "\t<rect id=\"referenceFrame\" fill=\"none\" stroke=\"black\" stroke-width=\"".nearest($roundVal,$Xwidth*0.0005)."\" x=\"$minXCoor\" y=\"$maxYCoor\" width=\"$Xwidth\" height=\"$Ywidth\" />\n";
		}
	}
}