Michal ZimmermannPieces of knowledge from the world of GIS.

Articles tagged with svg tag

Automated Map Creation With QGIS, PostGIS, Python, SVG and ImageMagick

As mentioned in QGIS Tips For Collaborative Mapping we’re in the middle of crop evaluation project at CleverMaps.

With the QGIS workflow up and running, I’ve been focused on different QGIS related task: automatic map generation for land blocks that meet certain conditions. The logic behind identifying such land blocks is as follows:

Let’s assume that with several lines of SQL code we can store these mentioned above in a table called land_blocks with geometries being the result of calling ST_Union() over parcels for each land block.

Map composition

Every map should feature following layers:

Labels should be visible only for the featured land block (both for the land parcels and the land block itself. The whole map scales dynamically, showing small land blocks zoomed in and the large ones zoomed out.

Every map features additional items:

Atlas creation

Now with requirements defined, let’s create some maps. It’s incredibly easy to generate a series of maps with QGIS atlas options.

Atlas generation settings

Coverage layer is presumably the only thing you really need - as the name suggests, it covers your area of interest. One map will be created for each feature in this layer, unless you decide to use some filtering - which I did.

Output filenames can be tweaked to your needs, here’s what such a function might look like. If there is a slash in the land block ID (XXXXXXX/Y), the filename is set to USER-ID_XXXXXXX-00Y_M_00, USER-ID_XXXXXXX-000_M_00 otherwise.

CASE WHEN strpos(attribute($atlasfeature, 'kod_pb'), '/') > -1
    THEN
        ji || '_' ||
        substr(
            attribute($atlasfeature, 'kod_pb'), 0,
            strpos(attribute($atlasfeature, 'kod_pb'), '/')+1 -- slash position
        ) || '-' ||
        lpad(
            substr(
                attribute($atlasfeature, 'kod_pb'),
                strpos(attribute($atlasfeature, 'kod_pb'), '/') + 2,
                length(attribute($atlasfeature, 'kod_pb'))
            ),
        3, '0') || '_M_00'
    ELSE
        ji || '_' || attribute($atlasfeature, 'kod_pb') || '-000_M_00'
END

Map scale & variable substitutions

Different land blocks are of different sizes, thus needing different scales to fit in the map. Again, QGIS handles this might-become-a-nightmare-pretty-easily issue with a single click. You can define the scale as:

With these settings, I get a map similar to the one below. Notice two interesting things:

Styling the map using atlas features

Atlas features are a great help for map customization. As mentioned earlier, in my case, only one land block label per map should be visible. That can be achieved with a simple label dialog expression:

CASE
    WHEN $id = $atlasfeatureid
    THEN "kod_pb"
END

QGIS keeps reference to each coverage layer feature ID during atlas generation, so you can use it for comparison. The best part is you can use IDs with any layer you need:

CASE
    WHEN attribute($atlasfeature, 'kod_pb') = "kod_pb"
    THEN "kod_zp"
END

With this simple expression, I get labels only for those land parcels that are part of the mapped land block. Even the layer style can be controlled with atlas feature. Land parcels inside the land block have blue borders, the rest is yellowish, remember? It’s a piece of cake with rule-based styling.

Atlas generation

When you’re set, atlas can be created with a single button. I used SVG as an output format to easily manipulate the map content afterwards. The resulting maps look like the one in the first picture without the text in the red rectangle. A Python script appends this to each map afterwards.

Roughly speaking, generating 300 maps takes an hour or so, I guess that depends on the map complexity and hardware though.

Adding textual content

SVG output is just plain old XML that you can edit by hand or by script. A simple Python script, part of map post-processing, loads SVG from the database and adds it to the left pane of each map.

SELECT
      ji,
      kod_pb,
      concat(
            '<g fill="none" stroke="#000000" stroke-opacity="1" stroke-width="1"
                  stroke-linecap="square" stroke-linejoin="bevel" transform="matrix(1.18081,0,0,1.18081,270.0,550.0)"
                  font-family="Droid Sans" font-size="35" font-style="normal">',
            kultura, vymery, vymery_hodnoty,
            vcs_titul, vcs_brk, vcs_brs, vcs_chmel,
            vcs_zvv, vcs_zv, vcs_ovv, vcs_ov, vcs_cur, vcs_bip,
            lfa, lfa_h1, lfa_h2, lfa_h3,
            lfa_h4, lfa_h5, lfa_oa, lfa_ob, lfa_s,
            natura, aeo_eafrd_text, dv_aeo_eafrd_a1,
            dv_aeo_eafrd_a2o, dv_aeo_eafrd_a2v, dv_aeo_eafrd_b1,
            dv_aeo_eafrd_b2, dv_aeo_eafrd_b3, dv_aeo_eafrd_b4,
            dv_aeo_eafrd_b5, dv_aeo_eafrd_b6, dv_aeo_eafrd_b7,
            dv_aeo_eafrd_b8, dv_aeo_eafrd_b9, dv_aeo_eafrd_c1,
            dv_aeo_eafrd_c3, aeko_text, dv_aeko_a, dv_aeko_b, dv_aeko_c,
            dv_aeko_d1, dv_aeko_d2, dv_aeko_d3, dv_aeko_d4, dv_aeko_d5,
            dv_aeko_d6, dv_aeko_d7, dv_aeko_d8, dv_aeko_d9, dv_aeko_d10,
            dv_aeko_e, dv_aeko_f, ez, dzes_text, rep, obi, seop, meop, pbz, dzes7,
            '</g>'
      ) popis
FROM (...);

Each column from the previous query is a result of SELECT similar to the one below.

SELECT concat('<text fill="#000000" fill-opacity="1" stroke="none">BrK: ', dv_brk, ' ha / ', "MV_BRK", ' ha;', kod_dpz, '</text>') vcs_brk

The transform="matrix(1.18081,0,0,1.18081,270.0,550.0) part puts the text on the right spot. Great finding about SVG is that it places each <text> element on the new line, so you don’t need to deal with calculating the position in your script.

Scale adjustment is done with a dirty lambda function.

content = re.sub(r"&gt;(\d{1,3}\.\d{3,5})&lt;/text&gt;", lambda m :"&gt;    " + str(int(round(float(m.group(1))))) + "&lt;/text&gt;", old_map.read())

SVG to JPEG conversion

We deliver maps as JPEG files with 150 DPI on A4 paper format. ImageMagick converts the formats with a simple shell command:

convert -density 150 -resize 1753x1240 input.svg output.jpg

Conclusion

I created pretty efficient semi-automated workflow using several open source technologies that saves me a lot of work. Although QGIS has some minor pet peeves (scale with decimal places, not showing the entire feature, not substituting variables at times), it definitely makes boring map creation quite amusing. The more I work with big data / on big tasks, the more I find Linux a must-have.

The whole process was done with QGIS 2.10, PostGIS 2.1, PostgreSQL 9.3, Python 2.7, ImageMagick 6.7.

Animating SVG Maps With SMIL

Using SVG to build web maps have both pros and cons and to be honest I don’t know any serious map/GIS project built on top of SVG. However, as a part of my job at university, I was forced to use both SVG and SMIL to produce animated web map (see the small version below or the big one at GitHub) and I’d like to share my findings.

Data preprocessing

I chose Natural Earth dataset both for basemap and thematic layer:

I decided that animation should go like this:

  1. Load basemap and Vaclav Havel airport (PRG).
  2. Animate destinations one by one. They are revealed in order of their distance from PRG.
  3. Animate airways.
  4. Once airways are animated, animate airplanes along their path from PRG to their destination in order of their time of departure.
  5. Profit.

My goal was to create an animation of all departures from Vaclav Havel airport during one day. These data can be obtained at FlightStats, I didn’t find a way make this process automatic though. OpenFlights might be better source then.

SVG creation

Kartograph is a great tool both for SVG generation and scripting. What a pity it’s probably a dead project according to the last commit date. After installing Python part of library used to create SVG files out of vector geometries, it can be run with something like this:

kartograph --output map.svg --pretty-print --style style.css config.json

Pretty self-explanatory, let’s have a look at config file:

{
    "layers": {
        "countries": {
            "src": "ne_50m_admin_0_countries/ne_50m_admin_0_countries.shp",
            "attributes": ["name"]
        },
        "airports": {
            "src": "ne_10m_airports/ne_10m_airports_prg.shp",
            "attributes": ["name", "abbrev"]
        },
        "travels": {
            "src": "ne_10m_airports/travels.shp",
            "attributes": ["time", "distance"]
        },
        "grid": {
            "special": "graticule",
            "latitudes": 10,
            "longitudes": 10
        }
    },
    "proj": {
        "id": "satellite",
        "lon0": 0.0,
        "lat0": 48.0,
        "dist": 45,
        "up": 15
    },
    "bounds": {
        "mode": "bbox",
        "data": [-180, -90, 180, 90],
        "padding": 1
    },
    "export": {
        "round": 1,
        "width": 1000,
        "ratio": 1
    }
}

It is possible to adjust map settings in many different ways. The most important/interesting:

You can change SVG look with simple CSS, just be sure to use layer names as CSS ids:

#airports {
    fill: #CC0000;
    fill-opacity: 0;
    stroke: #660000;
    stroke-opacity: 0;
}

#countries {
    fill: #e6deb4;
    stroke: #a59f81;
}

#grid {
    stroke: #d0d0d0;
    stroke-width: .3px;
}

#travels {
    stroke: #1f78b4;
    stroke-opacity: 0;
    stroke-dasharray: 5,5;
}

Data adjustment & animation

SMIL is a XML based language for multimedia representation. It comes ready for timing, animation, visual transitions etc. I guess it might be considered easier to read for a web development beginner. Once you start using it, you immediately realize it suffers from the same disease like XML does: it is so wordy!

Let’s get back to my example. To animate airports one by one, let’s give them unique ids, so they look something like:

&lt;circle id="brs" stroke-opacity="0" fill-opacity="0" cx="476.597304864" cy="539.487783171" data-abbrev="BRS" data-name="Bristol Int'l" r="3"/&gt;

That’s something you do by hand as kartograph doesn’t give ids to SVG elements. Once you’re done with that, you can run SMIL animation. If you look closer at the final map, you’ll notice there are three properties animated for each airport: fill opacity, stroke opacity and radius. Each property needs to use separate SMIL <animate />, which might look like the one below:

&lt;animate attributeName="fill-opacity"
    id="kos_ani_fo"
    from="0"
    to="1"
    begin="osr_ani.end"
    dur="0.25s"
    fill="freeze"
    xlink:href="#kos"
/&gt;
&lt;animate attributeName="stroke-opacity"
    id="kos_ani_so"
    from="0"
    to="1"
    begin="osr_ani.end"
    dur="0.25s"
    fill="freeze"
    xlink:href="#kos"
/&gt;
&lt;animate attributeName="r"
    id="kos_ani_r"
    from="10px"
    to="3px"
    begin="osr_ani.end"
    dur="0.25s"
    xlink:href="#kos"
/&gt;

I guess you get the idea how long this would take for more airports. Make sure to notice that SMIL can start animation based on another animation’s end (osr_ani.end) - that’s pretty neat.

Airways animation works almost the same. First, add unique id to each airway:

&lt;path d="M550.9,562.9L568.0,495.0 " id="travel-arn"/&gt;

Second, start animation after all the airports are visible on the map. Notice the initial definition of d attribute - it’s a line with zero length.

&lt;animate attributeName="d"
    id="path_ani"
    from="M550.9,562.9L550.9,562.9"
    to="M550.9,562.9L568.0,495.0"
    begin="icn_ani_r.end"
    dur="3s"
    xlink:href="#travel-arn"
/&gt;

Once airways animation has finished, let airplanes fly around the globe with a simple JavaScript function:

/**
 * @param  number coef  scale radius by number of flights to the given destination
 * @param  string flight_id
 */
var circle = function(coef, flight_id, timeshift) {
    var svgns = "http://www.w3.org/2000/svg";
    var svgDocument =document;
    var motion = svgDocument.createElementNS(svgns,"animateMotion");
    var animation = svgDocument.createElementNS(svgns,"animate");
    var shape  = svgDocument.createElementNS(svgns, "circle");
    var time = 15 + timeshift;
    var dur = document.getElementById(flight_id).getAttributeNS(null, "data-dist")/100;
    motion.setAttribute("begin", time + "s");
    motion.setAttribute("dur", dur + "s");
    motion.setAttribute("path", document.getElementById(flight_id).getAttributeNS(null, "d"));
    motion.setAttribute("xlink:href", "#" + flight_id);
    motion.setAttribute("id", flight_id + "_motion");

    animation.setAttribute("attributeName", "opacity");
    animation.setAttribute("from", "1");
    animation.setAttribute("to", "0");
    animation.setAttribute("begin", time + dur + "s");
    animation.setAttribute("dur", "0.1s");
    animation.setAttribute("fill", "freeze");


    shape.setAttributeNS(null, "r",  1*coef);
    shape.setAttributeNS(null, "fill", "1f78b4");
    shape.setAttributeNS(null, "stroke", "1f78b4");
    shape.setAttribute("id", "airplane-" + flight_id);
    shape.appendChild(motion);
    shape.appendChild(animation);

    document.getElementById("airplanes").appendChild(shape);
}

SMIL with SVG seems to be interesting option for web map animation, a bit lengthy though. Syncing animations can easily become pain in the ass (see StackOverflow thread). Never call your function animate - there is namesake function defined in Web Animations API that makes animation crash in Chrome. <animateMotion /> is a great tool to animate elements along path.