Designing an Australian Bushwalking Map - Part 1

In March 2019 we quietly launched the new BeyondTracks map and I wanted to share the thinking and design principles behind it.

The map was designed with the primary goal to be useful for bushwalking in Australia, both planning before you set out and on the ground in the country. With that in mind, together with my experiences with maps I wanted to ensure the following criteria were met.

1. Show enough detail

This is by far my biggest frustration with most other maps I’ve used so far. They show a mostly bare and empty map at low zooms, only revealing detail when you zoom in. That may be okay if you know exactly which part of the white space to zoom in on, but maps should be accessible to beginers too.

Compare the default OpenStreetMap style with our map. At this view, OpenStreetMap Carto shows the state borders and one road, we show all roads and place names around the area all without it feeling cluttered.

Left: OpenStreetMap default - Right: BeyondTracks map

Most traditional online maps I’ve seen assign a fixed minimum zoom level based on the feature’s classification, for example show all highways at zoom 6, all residential roads at zoom 14, show all camp sites at zoom 16.

Label density as a clutter heuristic

Our approach is different we try to keep a contstant density so we never show too little or too much detail at once. At the core our implementation is very simple.

  1. Assign a ranking for all features which contribute an icon or label to the map. eg. camp site above toilets, place name above a cafe, etc. This is specific to our bushwalking use case.
  2. Pick a static density of icons/labels, for example one every 100px. If we are happy to clutter the map with labels we could do one every 50px or if we want less labels one every 200px.
  3. Draw icons and labels on the map based on our ranking from (1) while ensuring the density for each zoom level never exceeds (2).

This ensures we never have too much detail while still ensuring that one lone camp site, the only feature within a 100km radius will show up at when zoomed out.

I do this by using LabelGrid from Mapbox’s PostGIS Vector Tile Utils, which assigns a hash of a geometry’s position based on grid size determined by our static density and zoom level. So each feature will get a LabelGrid for each zoom level.

-- return the label grid hash for this geometry and zoom
CREATE OR REPLACE FUNCTION BT_LabelGrid(geom geometry, zoom integer) RETURNS text AS $$
    SELECT LabelGrid(ST_Transform(geom, 3857), 64 * ZRes(zoom)::numeric)
$$ LANGUAGE SQL;
-- create a materialized table for each label including each zoom level labelgrid
CREATE UNLOGGED TABLE labels_grid AS (
    SELECT
        "table",
        ROW_NUMBER() OVER (ORDER BY "table" ASC) AS row_number,
        feature,
        rank,
        -- label grids are from the zoom + 1 since at zoom 10.9 we want z11 grids not z10
        BT_LabelGrid(geom, 1) AS labelgrid_0,
        BT_LabelGrid(geom, 2) AS labelgrid_1,
        BT_LabelGrid(geom, 3) AS labelgrid_2,
        BT_LabelGrid(geom, 4) AS labelgrid_3,
        BT_LabelGrid(geom, 5) AS labelgrid_4,
        BT_LabelGrid(geom, 6) AS labelgrid_5,
        BT_LabelGrid(geom, 7) AS labelgrid_6,
        BT_LabelGrid(geom, 8) AS labelgrid_7,
        BT_LabelGrid(geom, 9) AS labelgrid_8,
        BT_LabelGrid(geom, 10) AS labelgrid_9,
        BT_LabelGrid(geom, 11) AS labelgrid_10,
        BT_LabelGrid(geom, 12) AS labelgrid_11,
        BT_LabelGrid(geom, 13) AS labelgrid_12,
        BT_LabelGrid(geom, 14) AS labelgrid_13
    FROM
        labels
);

Finally assigning a minzoom per feature ensuring only the single highest ranked feature per each LabelGrid.

-- based on the assigned ranking and label grid, assign each label feature a
-- minzoom for when it can begin appearing
CREATE TABLE labels_minzoom AS (
    SELECT
        DISTINCT ON (row_number)
        "table",
        feature,
        rank,
        minzoom
    FROM
    (
        (
            SELECT DISTINCT ON (labelgrid_0) "table", row_number, feature, rank, 0 AS minzoom FROM labels_grid ORDER BY labelgrid_0, rank, row_number
        ) UNION (
            SELECT DISTINCT ON (labelgrid_1) "table", row_number, feature, rank, 1 AS minzoom FROM labels_grid ORDER BY labelgrid_1, rank, row_number
        ) UNION (
            SELECT DISTINCT ON (labelgrid_2) "table", row_number, feature, rank, 2 AS minzoom FROM labels_grid ORDER BY labelgrid_2, rank, row_number
        ) UNION (
            SELECT DISTINCT ON (labelgrid_3) "table", row_number, feature, rank, 3 AS minzoom FROM labels_grid ORDER BY labelgrid_3, rank, row_number
        ) UNION (
            SELECT DISTINCT ON (labelgrid_4) "table", row_number, feature, rank, 4 AS minzoom FROM labels_grid ORDER BY labelgrid_4, rank, row_number
        ) UNION (
            SELECT DISTINCT ON (labelgrid_5) "table", row_number, feature, rank, 5 AS minzoom FROM labels_grid ORDER BY labelgrid_5, rank, row_number
        ) UNION (
            SELECT DISTINCT ON (labelgrid_6) "table", row_number, feature, rank, 6 AS minzoom FROM labels_grid ORDER BY labelgrid_6, rank, row_number
        ) UNION (
            SELECT DISTINCT ON (labelgrid_7) "table", row_number, feature, rank, 7 AS minzoom FROM labels_grid ORDER BY labelgrid_7, rank, row_number
        ) UNION (
            SELECT DISTINCT ON (labelgrid_8) "table", row_number, feature, rank, 8 AS minzoom FROM labels_grid ORDER BY labelgrid_8, rank, row_number
        ) UNION (
            SELECT DISTINCT ON (labelgrid_9) "table", row_number, feature, rank, 9 AS minzoom FROM labels_grid ORDER BY labelgrid_9, rank, row_number
        ) UNION (
            SELECT DISTINCT ON (labelgrid_10) "table", row_number, feature, rank, 10 AS minzoom FROM labels_grid ORDER BY labelgrid_10, rank, row_number
        ) UNION (
            SELECT DISTINCT ON (labelgrid_11) "table", row_number, feature, rank, 11 AS minzoom FROM labels_grid ORDER BY labelgrid_11, rank, row_number
        ) UNION (
            SELECT DISTINCT ON (labelgrid_12) "table", row_number, feature, rank, 12 AS minzoom FROM labels_grid ORDER BY labelgrid_12, rank, row_number
        ) UNION (
            SELECT DISTINCT ON (labelgrid_13) "table", row_number, feature, rank, 13 AS minzoom FROM labels_grid ORDER BY labelgrid_13, rank, row_number
        ) UNION (
            -- at zoom 14 we show everything
            SELECT "table", row_number, feature, rank, 14 AS minzoom FROM labels_grid ORDER BY rank, row_number
        )
    ) AS t
    ORDER BY row_number, minzoom
);

tippecanoe‘s drop features by density takes care of line and polygon features but to gain more control over how this happens in the future the plan is to apply a similar preprocessing step in PostGIS first to pass each line and polygon feature a minzoom.

2. Cater for both novice and experienced map readers

On one hand novices generally find contour lines confusing, adding needless complexty and clutter to the map, on the other hand experienced map readers will seek them out and be frustrated without them.

I’ve tried to make the essential information bold and clear, and other secondary or more advanced information still present but fainer. Novice users should find it eaiser to filter extra information out, but it’s still there for the trained eye.

See how the Castle Walking track is so faint it's almost invisible on OpenStreetMap Carto, on the right our BeyondTracks map makes walking tracks more bold.
BeyondTracks map shows contours only at high zooms

3. Explain what each feature is

You generally have two main ways of communicating what a feature is on the map. Through graphics such as an icon, pattern, color or line and through text.

Use descriptive names, not just proper names

Graphics can communicate what the feature is most effeciently, so we try to make use of icons on the map, however symbols come with assumed knowledge, the more specialised that icon is the less likely you’ve seen it before. So in keeping with (2) sometimes novice users won’t know what some icons are, so we endevour to both use an icon for fast communication and text so that if the icon is not understood the text is the backup.

"pipeline" is used as a label description to