Sunday, January 11, 2015

Telling inside from outside using PostGIS and Mapnik

Nice labeling of administrative boundaries appears to have been a challenge for Mapnik users, and I've certainly not seen a good summary on the Web of how to render administrative boundaries attractively and legibly. In some recent experiments, I found what appears to be a scheme that others can leverage. Read on for the details.

I recently did an update to my work-in-progress of a hikers' map of the US Northeast, and decided to revisit how I handled the shading of the map. Another mapper had shown me a project of his, where the background of the map was rendered according to the National Land Cover Database - and it clearly provided useful information for a hiker, particularly those of us who occasionally venture off the marked trails.

Using landcover (overlaid with hill shading) as the base shading of the map left me with a problem: my previous map had used fill colours as a way to distinguish land ownership and regulatory status. In addition to answering the question of, "will hiking up this ridge have me pushing through the spruce?" I wanted to answer questions like, "is this area designated as Wilderness?" (Different camping regulations.) "Do I need a New York City Watershed permit to hike here?" and so on.

One way that I've seen printed maps handle the desire to overlay multiple types of area features is for them to outline an area and then use some special treatment (hachure, stipple, shading) along the inner side of the outline to indicate the information. Trying to use this sort of treatment with Mapnik raises the question: which side is the inner side? That's where I got to the last time that I thought about using this sort of treatment, and got no satisfactory answer. OSM's polygons do not appear to be wound in a consistent direction.

But this time, I stumbled upon a PostGIS function that I'd previously missed: ST_ForceRHR. This is a call that accepts a geometry (polygon or multipolygon), and imposes on it the Right Hand Rule. It returns the same geometry, with the borders listed so that along the direction of a line, the interior of the area is always on the right-hand side. (That is, it walks around polygons in a clockwise direction.)

The right-hand rule was exactly the missing piece that I needed. All that I needed for my wilderness areas, state parks, protected watersheds, and what not was to make a little semitransparent PNG with shading on one side, like this one.

Dashed line shaded on lower side
Dashed line, shaded on lower (inner) side

We make a style that uses a LinePatternSymbolizer to render the line that's shaded on one side:

  <!--Miscellaneous area features from OSM -->
  <Style name="osm-misc-area">
    <Rule>
      <MaxScaleDenominator>750000</MaxScaleDenominator>
      <Filter>
        [leisure] = 'playground' or
 [leisure] = 'golf_course' or
 [landuse] = 'recreation_ground' or
 [leisure] = 'recreation_ground' or
 [landuse] = 'village_green'
      </Filter>
      <LinePatternSymbolizer file="graphics/7e5-border.png"/>
    </Rule>
    <!-- many more rules for other types of landuse -->
  </Style>

And we feed it with an area query that uses ST_ForceRHR. As with most queries with subqueries, we need to use ST_Intersects to make sure that the geometry index gets used.

  <Layer name="recreation-lands-osm" srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +units=m +k=1.0 +no_defs">
    <StyleName>recreation-land-osm</StyleName>
    <Datasource>
      <Parameter name="type">postgis</Parameter>
      <Parameter name="dbname">gis</Parameter>
      <Parameter name="estimate_extent">
        false
      </Parameter>
      <Parameter name="extent">
        -8905831.039562456, 4865981.220634319, -7458419.471954359, 6274868.52598669
      </Parameter>
      <Parameter name="geometry_field">rhr</Parameter>
      <Parameter name="table">
        (SELECT ST_ForceRHR(way) AS rhr, name, way_area as shape_area
         FROM planet_osm_polygon
         WHERE ST_Intersects(ST_SetSRID(!bbox!, 3857), way)
         AND (leisure IN ('park', 'nature_reserve', 'common', 
                          'playground', 'garden', 'golf_course', 
                          'recreation_ground')
              OR landuse IN ('forest', 'vineyard', 'conservation', 
                             'recreation_ground', 'village_green', 
                             'allotments') 
              OR "natural" IN ('wood') 
              -- many more types of areas
             ) ) AS areas
      </Parameter>
    </Datasource>
  </Layer>

And the resulting rendering is just as I hoped: a thin dashed line with a green inner highlight on the natural areas.

Map, with natural areas showing a green inner border
Map, with natural areas showing a green inner border

Then it occurred to me: If we combine the right-hand rule with the list placement type on a TextSymbolizer, we can finally do proper labeling of administrative boundaries. Given the right-hand rule, we know that if a line is going left-to-right, the interior is below the line, and conversely, if it is going right-to-left, the interior is above the line. We can adjust dy accordingly to place a label on the correct side of the line.

  <!-- Attempt at edge labels on admin boundaries -->
  <Style name="admin-edge-label">
    <Rule>
      &minz8;
      <TextSymbolizer avoid-edges="true" clip="false"
   face-name="MartinGotURWTMed Italic"
   size="12"
   halo-radius="2"
   fill="black"
   halo-fill="transparent"
   dy="-8"
   placement-type="list"
   placement="line"
   spacing="500"
   max-char-angle-delta="30"
   upright="right_only">
 [name]
 <Placement upright="left_only"
     dy = "9">
   [name]
 </Placement>
      </TextSymbolizer>
    </Rule>
  </Style>

The layer specification is similar to the one for land use. The SQL query looks like:

        (SELECT ST_ForceRHR(way) AS rhr,
                name 
         FROM &db_osm_polygon_table;
         WHERE ST_Intersects(ST_SetSRID(!bbox!, 3857), way)
         AND "boundary"='administrative'
  AND admin_level IN ('2', '4', '6')) AS outlines

And again, it performs perfectly. Country, state and county names come out facing each other across the boundary lines.

Map, with labels on a state line
Map, with labels on a state line

(I am oversimplifying here, but only slightly. I'm actually rendering these labels twice, according to the recommendations at http://mapnik.org/news/2012/04/20/smart-halos/. Rather than using the dst-over compositing operator, however, I'm rendering the image with fill color and the image with line art separately, and compositing them in Python.

1 comment:

Just said...

Great post! Although now 7 years later, still relevant!
Got your approach working in PostGIS+Mapnik (3.0.22) XML with some local changes:

- the "RHR" function in PostGIS is now ST_ForcePolygonCW()
- Applied that function in the DB (generated from osm2pgsql) as postprocessing:
UPDATE planet_osm_polygon SET way = ST_ForcePolygonCW(way) WHERE "boundary" = 'administrative';
- mapnik XML Layer query is then simply: (SELECT way,boundary,admin_level,name FROM planet_osm_polygon WHERE boundary IN ('administrative')) AS borders (on ST_Intersects needed).
- the Style Rule *_right-only should be *-only (use dash)
- in both Placements, 'upright=left|right-only', within the Rule had to use negative 'dy' like dy="-6" to get proper results (Dutch Provinces admin level 4).

But now works like a charm! Will make Open Source later.