OpenStreetMap

Rendering Route Shields from Route Relations in Mapnik

Posted by asciiphil on 13 October 2010 in English (English)

[For some reason I can't get the command and config excerpts below to look like I want, which is basically what I'd get if I put them in a <pre> block. I've done the best I can.]

I've been getting back to map rendering, and one of the things that bothers me about pretty much everyone's rendering is that they either don't render US route shields or they go through hackery (in my opinion) to turn road ref tags into a shield plus a number.

I've wanted for a while to use the data in route relations for shield rendering. At least in the US, they use separate tags for network and route number, which makes picking the right shield easier. They also handle the case of a single road belonging to multiple routes better than the road's ref tag does.

The drawbacks are in the way that osm2pgsql puts relations into the database. When a way is a road and a member of a route relation, osm2pgsql creates one row with the road's tags and a separate row with the same path as the road but the tags from the relation. As far as I can tell, there's no simple link between them. This means that you can't use the road's highway key to determine visibility of the shield, which means, among other things, that you have to possibility of a shield being rendered at a lower zoom level than its road.

If the data was imported in slim mode, the planet_osm_rels table does contain a link between a road and its relations. I tried using it directly, but that proved to be too slow for everyday use. I ended up creating another table and running queries off of that. The rest of this diary is about what I did.

My specific requirements were: if a road is a member of a correctly-tagged route relation, use the appropriate shield with the relation's ref tag. If a road is a member of an incorrectly-tagged route relation, show the relation's ref tag on a generic shield. If the road is not a member of any route relations but has its own ref tag, show that on a generic shield. Highway names are not relevant to this rendering; they can be rendered from the planet_osm_line table directly. A nice additional feature would be if contiguous ways with the same route membership were aggregated before Mapnik saw them so the shields were evenly spaced along the way. (The normal rendering tends to bunch up shields (and names) along short ways like bridges and tunnels.)

With that in mind, I made a table with the absolute minimum amount of information necessary:

CREATE TABLE planet_osm_route (
  highway TEXT,
  road_ref TEXT,
  network TEXT,
  route_ref TEXT,
  way GEOMETRY);

Next was populating it with data from the osm2pgsql import. The following statement takes all the roads that are in route relations, cross joins each road with each relation it's in, groups them together by route and original road ref, then groups them again just by route. The reason for the double grouping is that I throw all the original ref tags into the road_ref field and I'd like the minimize the duplication there. I use a chain of PostGIS functions to group all the road LINESTRINGs into a MULTILINESTRING, merge contiguous lines together within the MULTILINESTRING, and then split the resulting geometry up into separate LINESTRINGs again. This is somewhat resource intensive, but I really like the results.

INSERT INTO planet_osm_route
  SELECT highway, string_agg(road_ref, ';') AS road_ref, network, route_ref,
         (ST_Dump(ST_LineMerge(ST_CollectionExtract(ST_Collect(way),2)))).geom AS way
    FROM (SELECT road.highway, road.ref AS road_ref,
             route.network, route.ref AS route_ref, route.osm_id,
             ST_Collect(road.way) AS way
            FROM planet_osm_line AS road
             JOIN (SELECT id, unnest(parts) AS part
             FROM planet_osm_rels) AS rels
             ON road.osm_id = rels.part
             JOIN (SELECT DISTINCT ref, osm_id, network FROM planet_osm_line) AS route
             ON -rels.id = route.osm_id
            WHERE road.highway IS NOT NULL
             AND route.ref IS NOT NULL AND route.network IS NOT NULL
            GROUP BY route.osm_id, road.highway, road.ref, route.network,
             route.ref) AS route_by_ref
    GROUP BY osm_id, highway, network, route_ref
    ORDER BY way;

That took about four hours for a US extract on my system, despite a bunch of things that were running and maxing out the disks to begin with.

The next step is to add in the roads that aren't in route relations. I did the same dance with the ways to get them as combined as possible.

INSERT INTO planet_osm_route
  SELECT road.highway, road.ref AS road_ref,
         route.network, route.ref AS route_ref,
         (ST_Dump(ST_LineMerge(ST_Collect(road.way)))).geom AS way
    FROM planet_osm_line AS road
      LEFT JOIN (SELECT id, unnest(parts) AS part
             FROM planet_osm_rels) AS rels
        ON road.osm_id = rels.part
      LEFT JOIN planet_osm_line route
        ON -rels.id = route.osm_id
    WHERE road.highway IS NOT NULL
          AND road.ref IS NOT NULL
          AND (route.ref IS NULL OR route.network IS NULL)
    GROUP BY road.highway, road.ref, route.network, route.ref
    ORDER BY way;

That took about half an hour on my system.

Finally, I added the necessary index on the table. (I did this last at the recommendation of pretty much every PostgreSQL document that mentions bulk imports.)

CREATE INDEX planet_osm_route_index ON planet_osm_route USING GIST (way GIST_GEOMETRY_OPS)

That took about an hour.

Those are really the hard bits. Setting up the rendering was easier.

For the shields, I grabbed blank Interstate, US, and generic state shields from Wikipedia (which has them under a public domain license) and rendered the SVGs to PNGs at a height of 24 pixels. At that height, the numbers for interstates should be 10 points tall and the US highways should be 12 points tall.

The Mapnik stylesheets should be pretty obvious from here, but I'll give an overview of them anyway. For clarity's sake, I'll cut out the rules for selective rendering based on highway type, and some of the more repetitive bits.

<Style name="interstateshields">
    <Rule>
        <Filter>[route_len] = 1 or [route_len] = 2</Filter>
        <ShieldSymbolizer name="route_ref" face_name="DejaVu Sans Bold" size="10"
             fill="white" placement="line"
             file="symbols/I-blank.png"
             type="png" min_distance="45" spacing="750" />
    </Rule>
    <Rule>
        <Filter>[route_len] = 3</Filter>
        <ShieldSymbolizer name="route_ref" face_name="DejaVu Sans Bold" size="10"
             fill="white" placement="line"
             file="symbols/I-blank_wide.png"
             type="png" min_distance="45" spacing="750" />
    </Rule>
    <Rule>
        <Filter>[route_len] &gt; 3</Filter>
        <ShieldSymbolizer name="route_ref" face_name="DejaVu Sans Bold" size="10"
             fill="white" placement="line"
             file="symbols/shield-black-8.png"
             type="png" min_distance="45" spacing="750" />
    </Rule>
</Style>

<Style name="usshields">
    <Rule>
        <Filter>[route_len] = 1 or [route_len] = 2</Filter>
        <ShieldSymbolizer name="route_ref" face_name="DejaVu Sans Bold" size="12"
             fill="black" placement="line"
             file="symbols/US_blank.png"
             type="png" min_distance="45" spacing="750" />
    </Rule>
    <Rule>
        <Filter>[route_len] = 3</Filter>
        <ShieldSymbolizer name="route_ref" face_name="DejaVu Sans Bold" size="12"
             fill="black" placement="line"
             file="symbols/US_blank_wide.png"
             type="png" min_distance="45" spacing="750" />
    </Rule>
    <Rule>
        <Filter>[route_len] &gt; 3</Filter>
        <ShieldSymbolizer name="route_ref" face_name="DejaVu Sans Bold" size="10"
             fill="white" placement="line"
             file="symbols/shield-black-8.png"
             type="png" min_distance="45" spacing="750" />
    </Rule>
</Style>

<Style name="stateshields">
    <Rule>
        <Filter>[route_len] = 1 or [route_len] = 2</Filter>
        <ShieldSymbolizer name="route_ref" face_name="DejaVu Sans Bold" size="12"
             fill="black" placement="line"
             file="symbols/State_blank.png"
             type="png" min_distance="45" spacing="750" />
    </Rule>
    <Rule>
        <Filter>[route_len] = 3</Filter>
        <ShieldSymbolizer name="route_ref" face_name="DejaVu Sans Bold" size="10"
             fill="black" placement="line"
             file="symbols/State_blank.png"
             type="png" min_distance="45" spacing="750" />
    </Rule>
    <Rule>
        <Filter>[route_len] &gt; 3</Filter>
        <ShieldSymbolizer name="route_ref" face_name="DejaVu Sans Bold" size="10"
             fill="white" placement="line"
             file="symbols/shield-black-8.png"
             type="png" min_distance="45" spacing="750" />
    </Rule>
</Style>

<Style name="nonetworkroads">
    <Rule>
        <Filter>[road_len] = 1 or [road_len] = 2</Filter>
        <ShieldSymbolizer name="road_ref" face_name="DejaVu Sans Bold" size="10"
             fill="white" placement="line"
             file="symbols/shield-black-2.png"
             type="png" min_distance="45" spacing="750" />
    </Rule>
    <Rule>
        <Filter>[road_len] = 3</Filter>
        <ShieldSymbolizer name="road_ref" face_name="DejaVu Sans Bold" size="10"
             fill="white" placement="line"
             file="symbols/shield-black-3.png"
             type="png" min_distance="45" spacing="750" />
    </Rule>
    <Rule>
        <Filter>[road_len] = 4</Filter>
        <ShieldSymbolizer name="road_ref" face_name="DejaVu Sans Bold" size="10"
             fill="white" placement="line"
             file="symbols/shield-black-4.png"
             type="png" min_distance="45" spacing="750" />
    </Rule>

    <!-- And so on... -->
</Style>

<Layer name="interstateshields" status="on">
    <StyleName>interstateshields</StyleName>
    <Datasource>
        &dbsettings;
        &extents;
        <Parameter name="table">
            (select highway, route_ref, length(route_ref) AS route_len, way
            from planet_osm_route
            where highway IS NOT NULL and network = 'US:I'
            ) as interstateshields
        </Parameter>
    </Datasource>
</Layer>

<Layer name="usshields" status="on">
    <StyleName>usshields</StyleName>
    <Datasource>
        &dbsettings;
        &extents;
        <Parameter name="table">
            (select highway, route_ref, length(route_ref) AS route_len, way
            from planet_osm_route
            where highway IS NOT NULL and network IN ('US:US', 'US')
            ) as usshields
        </Parameter>
    </Datasource>
</Layer>

<Layer name="stateshields" status="on">
    <StyleName>stateshields</StyleName>
    <Datasource>
        &dbsettings;
        &extents;
        <!-- Add other states as needed. -->
        <Parameter name="table">
            (select highway, route_ref, length(route_ref) AS route_len, way
            from planet_osm_route
            where highway IS NOT NULL and network IN ('US:MD', 'US:PA', 'US:DE', 'US:VA', 'US:WV')
            ) as stateshields
        </Parameter>
    </Datasource>
</Layer>

<Layer name="nonetworkroads" status="on">
    <StyleName>nonetworkroads</StyleName>
    <Datasource>
        &dbsettings;
        &extents;
        <Parameter name="table">
            (select highway, road_ref, length(road_ref) AS road_len, way
            from planet_osm_route
            where highway IS NOT NULL and network IS NULL
            ) as nonetworkroads
        </Parameter>
    </Datasource>
</Layer>

The rendered result has been everything I've wanted in shield rendering, and all i need to do is spend about six hours dumping and reloading the table every time I do a batch of updates to my database.

I hope other people will find some utility in the work I've done.

Edit: By request, a screenshot that should illustrate several of the different classes of routes I'm mapping.

Partial TopOSM-based Rendering

This rendering has I-70 (an Interstate with a route relation), US 40 (a US highway with a route relation), US 29 (a US highway with a route relation whose ref tag is incorrect), MD 108 (a state road with a route relation), and MD 103 and MD 104 (state roads without route relations but with ref tags on the roads). MD 100 (a state road without a route relation) is also in the rendering, but Mapnik puts its closest shield just off the image to the lower right.

Comment from iandees on 13 October 2010 at 23:57

Can you post a screenshot of the result? Have you tried using the SVG symbolizer to allow for arbitrarily-sized symbols?

Hide this comment

Comment from asciiphil on 14 October 2010 at 00:28

I'll see about a screenshot. My current rendering setup is based off of TopOSM, which renders things into a lot of separate layers, which in turn makes one-off renderings more difficult. (Plus I'm doing the base layer differently and I probably have another four to five days of processing before it's ready to be integrated with the other layers.)

As for SVGs, I didn't think of that, since it's a relatively new development. I don't think it would buy me much; just not having to render the SVGs in the Gimp.

Hide this comment

Comment from asciiphil on 14 October 2010 at 01:50

Okay, sample rendering added (currently without the lowest, elevation-based layers or the highest layer, which would have town names).

Hide this comment

Comment from JohnSmith on 14 October 2010 at 02:16

I had a crack at this some time ago, plus we were able to build up a reasonable list of image files for the highway shields:

http://wiki.openstreetmap.org/wiki/Custom_Highway_Shields

Hide this comment

Leave a comment

Parsed with Markdown

  • Headings

    # Heading
    ## Subheading

  • Unordered list

    * First item
    * Second item

  • Ordered list

    1. First item
    2. Second item

  • Link

    [Text](URL)
  • Image

    ![Alt text](URL)

Login to leave a comment