-
Notifications
You must be signed in to change notification settings - Fork 2
Using the GroupSymbolizer
The current implementation's support for clusters of shields (to handle concurrent routes) depends on Mapnik's GroupSymbolizer to do the rendering.
The example on the page cited above is certainly informative about how information is to be presented to the symbolizer, but it doesn't really convey how to make the connection in practice from route relations in the database to a flattened table to presented to the renderer. This page attempts to close the gap. We work backwards from the data as they need to appear in the Mapnik layer into what has to happen in the database.
The example in the Mapnik wiki page isn't too far removed from the GroupSymbolizer that is used in practice. Rather than a profusion of conditionals, though, it simply presumes that every shield to be rendered has an external graphic file dedicated to it. That means that a single ShieldSymbolizer or PointSymbolizer can render everything. The rendering style looks like:
<Style name="road-shield">
<Rule>
<GroupSymbolizer start-column="1" num-columns="8"
placement="line" spacing="200"
clip="false">
<PairLayout/>
<GroupRule>
<Filter>[picture_%] != ''</Filter>
<ShieldSymbolizer file="[picture_%]"
fontset-name="book-fonts" size="10" fill="white"
>' '</ShieldSymbolizer>
</GroupRule>
</GroupSymbolizer>
</Rule>
</Style>
There are a number of subtleties involved in making a Mapnik layer that uses the style above. First, the database result is coming from a stored procedure. Mapnik therefore cannot simply query the database for the spatial reference system, and must be told what it is (in this case, it's Google Mercator, the system in which we intend to render). Mapnik's initial query to determine the valid extent will also not work (in fact, it will run for days on a continent-sized extract, eventually running out of memory), and so the extent of map coverage must be given. Finally, the column name (way) that holds the geometry, and a template table to show what the way column looks like, must be given. Omitting any of these - including specifying the SRID twice - will cause atrocious performance either at startup or when executing a query for rendering.
A separate layer is provided for each range of zoom levels, in order to control which ways are to get shields (motorways only, major roads only, all osm_roads, all highways), and to adjust the sizes of the shields.
<Layer name="road-shields-16-up"
status="on"
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"
maximum-scale-denominator="12500" >
<StyleName>road-shield</StyleName>
<Datasource>
<Parameter name="type">postgis</Parameter>
<Parameter name="dbname">gis</Parameter>
<Parameter name="estimate_extent">false</Parameter>
<Parameter name="extent">
-9462156.716111112, 3895303.962851664,
-7347086.391333333, 6106854.83403499
</Parameter>
<Parameter name="srid">3857</Parameter>
<Parameter name="geometry_field">way</Parameter>
<Parameter name="geometry_table">planet_osm_line</Parameter>
<Parameter name="table">
(SELECT way, id, picture_1, picture_2, picture_3, picture_4,
picture_5, picture_6, picture_7, picture_8
FROM planet_osm_query_shields_line_all(!bbox!,28))
AS pictures
</Parameter>
</Datasource>
</Layer>
At a minimum, the group symbolizer needs enough information to render the shields, all presented in a single row of the result set of a database query. This row must include a linear geometry for the route segment, and the shield information (the current implementation uses file names containing the symbols) as a separate column for each shield in the group. It is awkward to package the routes in which a way participates as a single row, when they start out as multiple database rows representing route relations, so it's easiest if we have a stored procedure to repackage the results. This requirement gives rise to the stored procedures, query_shields_line_all, query_shields_roads_all, query_shields_roads_major and query_shields_roads_motorway, to be found in the file, queryprocs.sql.in. Looking at the bottom of each of these procedures, you'll see the repackaging of an array of graphic file names as a set of columns. The set is limited to eight members, because that's the worst case known of route concurrencies in the US.
SELECT
q40.way AS way,
q40.gfiles[1] AS picture_1,
q40.gfiles[2] AS picture_2,
q40.gfiles[3] AS picture_3,
q40.gfiles[4] AS picture_4,
q40.gfiles[5] AS picture_5,
q40.gfiles[6] AS picture_6,
q40.gfiles[7] AS picture_7,
q40.gfiles[8] AS picture_8
FROM q40
Mapnik's PointSymbolizer and ShieldSymbolizer (which are both used to support a GroupSymbolizer both work with graphics that are stored in external files. Because of wanting careful control over font and layout, and because some routes have unique pictorial shields, the current implementation generates a graphic element for every distinct shield in the database. These are indexed in the osm_shield_graphics table, which has five columns:
Name | Type | Meaning |
---|---|---|
id | SERIAL PRIMARY KEY | Unique ID of this graphic element |
network | TEXT | network=* value of the route relation |
ref | TEXT | ref=* value of the route relation |
size | INTEGER | Desired size of the graphic, in pixels |
filename | TEXT | Path name of the graphic file to use |
For convenience, there is a gids_to_files procedure in queryprocs.sql.in that converts an array of integer unique ID's to an array of file names. Each of the four query procedures has an intermediate query right before the repackaging that retrieves the names. In each case it looks like:
SELECT q30.way as way,
q30.gids AS gids,
@PREFIX@_gids_to_files(gids) AS gfiles
FROM q30
If shields are rendered on OSM ways, there will be far too many shields on the map. The reason is that there will be a new way every time one of the attributes of a road (for example, number of lanes, speed limit, turn restrictions) changes. While the GroupSymbolizer has attributes such as minimum-path-length and minimum-distance to attempt to mitigate this problem, it tends to be ineffective. A reasonable value for minimum_path_length often suppresses labels on a route altogether, because there is no single stretch long enough to render!
What appears to be a better approach is to let PostGIS aggregate the segments that share a common set of routes into larger ways. There is a built-in function, ST_LineMerge that does the hard work. The next earlier subquery in the chain handles merging the lines. The GROUP BY clause in the query is working on an array of graphic ID's, so all ways that belong to a given combination of ways will group together.
SELECT ST_LineMerge(ST_Union(q20.way)) AS way,
q20.gids AS gids
FROM q20
GROUP BY gids
Of course, all the above depends on reliably finding the sets of concurrent ways for a given cluster of routes. A stock rendering database created from osm2pgsql cannot quite do this effectively. The only place that maintains the association between ways and the routes that follow them is the PREFIX_osm_rels table, and this table can be searched efficiently only by relation ID, not by geometry nor by the ID of a contained way. (The contained way ID's are maintained only as array elements, in a heterogeneous array that also contains relation and node ID's, so it's difficult, perhaps even impossible to make an index for these.)
The solution that the current implementation has chosen is to make two auxiliary tables, limited to route relations, that track route relation memberships. The first, PREFIX_osm_shieldroute is simply a table to list routes and retain their attributes.
Column | Type | Meaning |
---|---|---|
relid | BIGINT PRIMARY KEY | The OSM ID of the relation, which is known to be type=route |
route | TEXT | The value of the relation's route=* tag |
network | TEXT | The value of the relation's network=* tag |
ref | TEXT NOT NULL | The value of the relation's ref=* tag, or the empty string if the tag is absent. |
The second, PREFIX_osm_shieldway, tracks the members of route relations.
Column | Type | Meaning |
---|---|---|
relid | BIGINT | OSM ID of the route relation |
idx | INT NOT NULL | Position of the way in the route relation |
wayid | BIGINT | OSM ID of the member way |
role | TEXT | Role of the member way |
Given these two tables, retrieving a set of concurrent routes for a given way is a four-way join operation:
- Retrieve the ways of interest in the bounding box by querying PREFIX_osm_line or PREFIX_osm_roads.
- Join the result to the PREFIX_osm_shieldway table, so that each way expands to a set of rows, one for each shielded route in which the way participates.
- Join the result to the PREFIX_osm_shieldroute table to pick up network and reference for each shielded route.
- Join the result to the osm_shield_graphics table, to decorate each route with its graphics ID and cast out ones for which no shield is available. The SQL query to do this is somewhat verbose, but given the road map above should be fairly straightforward to interpret. All of the tables are indexed so that the individual joins are unique index scans, and the query is quite performant at rendering time.
SELECT ln.osm_id AS id,
ln.way AS way,
rt.network AS network,
rt.ref AS "ref",
g.id AS gid
FROM @PREFIX@_line ln
JOIN @PREFIX@_shieldway sw
ON sw.wayid = ln.osm_id
JOIN @PREFIX@_shieldroute rt
ON rt.relid = sw.relid
JOIN osm_shield_graphics g
ON g.route = rt.route
AND g.network = rt.network
AND g."ref" = (CASE WHEN rt."ref" IS NULL THEN '' ELSE rt."ref" END)
AND g."size" = size_wanted
WHERE ST_Intersects(ln.way, ST_SetSRID(bbox, 3857))
AND rt.route = 'road'
ORDER BY "network", "ref"