Vector - Geo - Topo

While Scalable Vector Graphics (SVGs) are very powerful when it comes to creating 2D image and map representations, they are not the first choice for encoding geospatial data. Many of the fancier graphics options are simply not needed in a file format to store accurate geographic information. Surveying in the real world mainly deals with straight lines between known latitude-longitude coordinates, so curve options are often omitted. This is one reason why I currently rely on filters to smooth the otherwise edgy shapes on my 3e WIP map. But… since why deal with geospatial formats in the first place? There are a few good reasons to keep them in mind early on:

  • Global frame of reference Unlike svgs, geographical data formats usually operate on a latitude-longitude coordinate system. This makes it easier to align different map artifacts with each other on the common globe map. Any scale or orientation differences are directly stored in the shape coordinates.

  • Framework support While map visualizations apps, like openlayers, often support svgs as map input, they are typically treated as images and not native vector features. This means that a lot of the functionality around vector shapes, e.g. the ability to edit them on the maps, is unavailable.

  • Data convertibility Using a standardized format also enables the conversion of data into forms readable by general geographic information system (GIS) applications. This allows users to edit the data with the software of their choice (or whatever they have access to).

Ideally we want to be able to switch between formats to take advantage of svg editing apps and high-quality rendering, and the platform support of geospatial data formats. To that end, I have decided to write my own (Inkscape) svg to geojson converter in javascript. Let’s take a quick look at the differences between these formats.

Take this simple vector shape; a four sided polygon with a triangular hole in it:

The following svg code would create this shape:

<svg height="200" width="500" xmlns="http://www.w3.org/2000/svg">
  <path d="M 10,10 490,30 490,170 10,190 z M 60,70 v 60 l 30,-30 z" style="fill:gray;stroke:darkgray;stroke-width:3" />
</svg>

The actual geometric information is highlighted in yellow and shows of some specific svg control characters, e.g. “M” and “z” which, in this case, denote where the two polygons begin and end (i.e. reconnect to the starting point). The “v” is a shorthand for a vertical line and therefore only needs a single coordinate following it. It being lowercase also indicates that the distance is relative to the previous point, so the second corner of the hole is at the coordinate (60,130), the relative line command “l” then moves to the third point at (90,100) before the “z” completes the triangle. Those are just examples of control characters, a complete list of all options can be found here.

One thing to note is that multiple paths can be grouped together in group elements (<g>), which usually contain transformations applied to all sub-elements (e.g. scaling). These hierarchical transformations need to be considered when converting coordinates to other coordinate systems.

Geojson

One of the simplest geospatial data formats is GeoJSON. It supports collections of features made up of points of points, (poly-)lines, and polygons, each with a “properties” attribute that can contain arbitrary metadata.

Here is how the previous shape would look like as a geojson file:

{ "type": "FeatureCollection",
  "features": [
    { "type": "Feature",
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [ [10.0, 10.0], [490.0, 30.0], [490.0, 170.0], [10.0, 190.0] ],
          [ [60.0, 70.0], [60.0, 130.0], [90.0, 100.0] ]
        ]
      },
      "properties": {
        "style": "fill:gray;stroke:darkgray;stroke-width:3"
      }
    }
  ]
}

This representation is obviously more verbose than its svg counterpart, but due to the lack of control characters and relative distances it is also more human-readable and easier to process. On very large datasets with a lot of geometry this verbosity can lead to increased file sizes, but at the current scale we should not run into issues with a full representation of Toril. Usually, if data size becomes a problem, TopoJSON is suggested as a solution. It converts GeoJson data into a more compact form by breaking up shapes into groups of polylines and ensures that polygons partially sharing an edge use a reference to the same coordinate data instead of duplicating any values. This doesn’t really help with the distinct shapes used in our map so far, but it could have some benefit with shared country borders later on.

Conversion

To get from an svg image to geojson data, we need to first define the extent of the image on the globe (Since geojson coordinate data is considered to be in the lat-long format). In our current use case of the 3e map, we can simply define the globe positions of the top left and bottom right corners of the image and derive an affine transformation matrix that can be applied to all coordinates of the svg. We use this matrix as a starting point and traverse the group hierarchy of our svg file by applying local group transformations to the base matrix, ultimately converting paths to transformed geojson feature coordinate lists. However, we can skip several group nodes that don’t contain useful data. Here’s an outline of the groups contained in my WIP map file:

<svg>
  <defs>
    [Filter/Pattern definitions]
  </defs>
  <g inkscape:label="Png maps" ...>
    [2D images ]
  </g>
  <g inkscape:label="Vector data" ...>
    <g inkscape:label="Land" ...>
      [ ... path elements of terrain type]
    </g>
    [ ... terrain groups]
  </g>
  <g inkscape:label="Filter styles" ...>
    [clones of vector groups with applied filters]
  </g>
</svg>

Only the members of the “Vector data” group contain shapes that need to be converted, all other groups can be ignored. Within Vector data, groups correspond to individual terrain types and they contain mostly single polygon paths. A few shapes are multipolygons containing holes (e.g. the Faerun land polygon, which has a hole for the Sea of Fallen stars). Some future terrain types like rivers and mountain ridges will be made up of polylines. Still, we need to be able to parse all characters that we can encounter in a “path” parameter. GeoJson’s lack of support for any curve primitives means that we can just draw straight lines from the start to the end point of any curve definition, effectively ignoring the curve parameters (this is mostly a fail save for external files, my current map does not use curves in its Vector data).

While building the geojson file, we can also fill the properties field with Inkscape information, in particular by setting the name of a feature to the “inkscape:label” data. (Svg shapes and layers all have unique ids, but inkscape stores layer and path names in this separate field to allow multiple objects to have the same label). Color and stroke type information can also be included this way.

An initial implementation of the converter can be found here. It parses each terrain type group to geojson and then creates an openlayers vector group on the world map for each.

Feature Creation

With an existing converter, we can also think about injecting additional features into the geojson data and svg image alike. This could range from additional points in a shape outline to make it look more smooth or jagged, to adding completely new shapes derived from existing vector data. An example (and sneak peak) would be the mountain terrain. Mountains have a bright and a shadowed flank with different visual details which require 2 different shapes to visualize. But tracing both sides separately would be a lot of work. Instead, it would be easier to just trace the entire mountain area (bright & shadowed) and draw an additional line for the mountains main ridge within it. The parser can then generate the two flank shapes automatically.

This is, however, already a sneak peek into the next post, where we look at the mountain terrain in detail.

Written on July 12, 2024