SVG Mapping in Perl: Bag o' Tricks

Anton Berezin <tobez@tobez.org>

Copenhagen, November 2013

Example of what we want to achieve

   

SVG == XML

<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024" height="768"
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  xmlns:attrib="http://www.carto.net/attrib/">
...
</svg>

SVG Lines

<line x1="10" y1="10" x2="290" y2="290" style="stroke:#ff0000;stroke-width:5"/>

SVG Rectangles

<rect x="30" y="30" width="240" height="90" style="fill:#a5bfdd"/>

SVG Circles

<circle fill="#652d86" stroke="black" cx="180" cy="30" r="10"/>

SVG Paths (Polylines)

<path style="fill:#0000ff;stroke-width:5;stroke:#ff0000"
  d="M10 10  40 290  70 70  290 40 z" />

SVG Paths (Splines)

<path d="M 20 150 Q 150 250 280 150"
  fill="none" style="stroke:#800000;stroke-width:5"/>

NaturalEarth

Geo::ShapeFile

use Geo::ShapeFile;

my $name = shift;

my $g = Geo::ShapeFile->new("ne_110m_admin_0_countries");
for my $i (1..$g->shapes()) {
    my $shape = $g->get_shp_record($i);
    my %db = $g->get_dbf_record($i);
    for my $part (1 .. $shape->num_parts) {
        my @points = $shape->get_segments($part);
        my @xy = map {
            [$_->[0]{X}, $_->[0]{Y}]
        } @points;
    }
}

A segment example

p[0][0]{X}, p[0][1]{X}==p[1][0]{X}, p[1][1]{X}==p[2][0]{X},
p[2][1]{X}==p[3][0]{X},...,p[N][1]{X}==p[0][0]{X}

A segment example

It is Sjælland, by the way...

Segmented Denmark

Getting sites coordinates

use Geo::ShapeFile;

my $name = shift;

my $g = Geo::ShapeFile->new("ne_10m_populated_places");
for my $i (1..$g->shapes()) {
    my %db = $g->get_dbf_record($i);
    for my $n (qw(NAMEPAR NAMEASCII NAME GN_ASCII)) {
        if ($name eq $db{$n}) {
            my @n = ($name);
            push @n, $db{ADM1NAME} if $db{ADM1NAME};
            push @n, $db{ADM0NAME} if $db{ADM0NAME};
            printf "%.2f %.2f: %s\n", $db{LATITUDE}, $db{LONGITUDE}, join ", ", @n;
            last;
        }
    }
}

Getting sites coordinates

$ ./coord Copenhagen
55.68 12.56: Copenhagen, Hovedstaden, Denmark
$

Problem: small places nobody heard of

$ ./coord Ashburn
$

"Ashburn, Virginia, unincorporated area in Loudoun County"

39°02′37″N 077°29′15″W <-- there

Problem: different places named the same

$ ./coord 'St. Petersburg'
27.77 -82.68: St. Petersburg, Florida, United States of America
59.94 30.32: St. Petersburg, City of St. Petersburg, Russia
$
$ ./coord London
37.13 -84.08: London, Kentucky, United States of America
42.97 -81.25: London, Ontario, Canada
51.50 -0.12: London, Westminster, United Kingdom
$

Projections? What's that?

Even with a better resolution, it is still ugly:

Geo::Proj4

Geo::Proj4

use Geo::Proj4;

$from  = Geo::Proj4->new("+proj=latlong +datum=NAD83");
$to    = Geo::Proj4->new("+proj=aea +lat_1=35 +lat_2=40 +lon_0=12");

$p = $from->transform($to, [$lon, $lat]);  # lon is "x", lat is "y"
# draw at $p->[0], $p->[1]

Choosing the right projection

$ proj -lP
aea : Albers Equal Area
        Conic Sph&Ell
        lat_1= lat_2=
aeqd : Azimuthal Equidistant
        Azi, Sph&Ell
        lat_0 guam
airy : Airy
        Misc Sph, no inv.
        no_cut lat_b=
...etc...
Currently, 133 projections.

Small countries and regions

Large countries and continents

Minimizes distortion of shape and linear scale between standard parallels. Better suited for East-West landmasses.

Choosing the right projection

Denmark

+proj=laea +lat_0=55.71 +lon_0=10.73

Denmark

Compare again with (LAT,LON) abomination.

Contiguous USA

+proj=aea +lat_1=29.5 +lat_2=45.5 +lon_0=-98

Contiguous USA + neighbours

plus Canada plus Mexico

Contiguous USA, unclipped

This is what was actually drawn on the previous slide.

Math::Polygon::Clip

use Math::Polygon::Clip;

my @clipped = polygon_fill_clip1(
    [0,0,$width,$height],
    @svg_points,
    $svg_points[0]);
if (@clipped < @svg_points) {
    @svg_points = @clipped;
}

Contiguous USA, clipped

Effects of Math::Polygon::Clip

$ ls -l usa*svg
-rw-r--r--  1 tobez  staff   7037 Nov 23 01:15 usa.svg
-rw-r--r--  1 tobez  staff  21965 Nov 23 01:15 usa-neib.svg
-rw-r--r--  1 tobez  staff  21204 Nov 23 01:15 usa-noclip.svg
-rw-r--r--  1 tobez  staff   9848 Nov 23 01:15 usa-clip.svg

Coordinate conversion (LON,LAT) -> SVG (X,Y)

# $svg_width, $svg_left, $svg_right are given
# $from and $to are projections
# $min_px, $min_py, $max_px, $max_py
# $lon and $lat are inputs

$svg_scale = $svg_width/($max_px - $min_px);
$svg_height = ($max_py-$min_py}) * $svg_scale;

$p = $from->transform($to, [$lon, $lat]);
($px, $py) = @$p;

$svg_x = $svg_left + ($px - $min_px) * $svg_scale;
$svg_y = $svg_top + ($max_py - $dy) * $svg_scale;

Working with SVG elements from JS

<div id="view_europe" class="map_view">
<embed id="svg_europe" src="europe.svg" type="image/svg+xml"/>
</div>
var e = svg.getElementById("Copenhagen_Stockholm");
if (max_utilization > 60) {
    $(e).attr("style", "stroke:#ff3366;stroke-width:5");
}

Loading SVG - event handling

function svg_init(svg_id) {
    var svg_embed = document.getElementById(svg_id);
    var svg = svg_embed.getSVGDocument();
    if (svg && svg.getElementById("svg_here")) {
        setup_svg(svg);
    } else {
        svg_embed.addEventListener("load", function() {
            var svg = svg_embed.getSVGDocument();
            setup_svg(svg);
        }, false);
    }
}
<rect id="svg_here" width="1" height="1" style="display:none;"/>

Too many straight lines

Dallas <-> Phoenix
Dallas <-> Los Angeles
Los Angeles <-> Phoenix

Adjusted lines

Dallas <-> Phoenix       adjust -10
Dallas <-> Los Angeles   adjust 20
Los Angeles <-> Phoenix  adjust 5

Adjusted lines

my $d = $adjustment_value;
my $cx = ($x1+$x2)/2;
my $cy = ($y1+$y2)/2;
my $ml = ($y2-$y1)/($x2-$x1);  # slope
my $mp = -1/$ml;   # slope of the perpendicular
# Perpendicular vector of length $d
my $dx = $d/sqrt($mp*$mp + 1);
my $dy = $mp*$dx;

Adjusted lines (cont.)

Spline control point calculation.

my $z0 = ($y2-$y1)*($cx + $dx - $x1) - ($x2-$x1)*($cy + $dy - $y1);
if ($z0 > 0) {  # "+" is to the left
    if ($d > 0) {
        $x = $cx - $dx;
        $y = $cy - $dy;
    } else {
        $x = $cx + $dx;
        $y = $cy + $dy;
    }
} else { # "-" is to the left
    if ($d > 0) {
        $x = $cx + $dx;
        $y = $cy + $dy;
    } else {
        $x = $cx - $dx;
        $y = $cy - $dy;
    }
}

Hit test bug in Mozilla

<svg xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 10 10">
  <style type="text/css">
    line{ stroke: #f00; stroke-width: 1px }
    line:hover{ stroke: #0f0 }
  </style>
  <line x1="0" y1="5" x2="100" y2="6"/>
</svg>

The above works everywhere but in Firefox.

Hit test bug in Mozilla

Questions?

   

Thank you! ☺