Americana Shield Renderer
The Americana shield renderer is a library intended to draw highway shields on a maplibre-gl-js vector-tiled map.
Shield rendering workflow
Rendering shields requires the following compoments:
-
Encode shield information in vector tiles. First, your tiles must contain the information which tells the shield renderer what shields to draw. In OpenMapTiles, shield information is encoded in the transportation_name
vector tile layer with a series of attributes named route_1
, route_2
, etc. Each attribute contains a text string which contains all of the information needed to determine which graphic to display, including numeric route number if the shield is numbered. However, this library allows you to specify how the shield information has been encoded, and it's possible to stitch together data from multiple fields when encoding shield data.
-
Expose shield information in a style layer. Next, route information must be exposed in a maplibre expression using image in a structured string containing the route information. For example, you might encode Interstate 95 as an image named shield|US:I=95
. Normally, the image expression is used to point to pre-designated sprites in a sprite sheet, but in thise case, we're pointing to a sprite which doesn't exist called shield|US:I=95
. This will trigger a styleimagemissing
event which allows the shield renderer to create the required graphic on the fly. As an example of how to encode shield information, see OSM Americana's highway_shield
style layer.
-
Create a callback that defines how route information is encoded. There are three parts to a route definition:
- The
network
string, which defines a network with a common shield shape, graphic, and color - The
ref
string, which defines a text sequence that should be drawn on top of the shield graphic - The
wayName
string, which defines a name, separate from the ref, that is used to determine which graphic to draw
let routeExtractor = (id) => {
let id_parts = id.split("|");
let network_ref = id_parts[1].split("=");
return {
network: network_ref[0],
ref: network_ref[1],
wayName: id_parts[2],
};
};
- (Optional) Create predicates that define which shields will be handled. For example, if all sprite IDs in your style that need a shield begin with the string
shield|
, this would look like:
let shieldPredicate = (imageID) => imageID.startsWith("shield");
This step can be skipped if all unhandled image IDs are shields.
Additionally, you can specify which networks will be handled. The example below ignores all nwn, lwn, ncn, etc network values:
let networkPredicate = (network: string) => !/^[lrni][chimpw]n$/.test(network);
-
Create shield definitions and artwork. The shield definition is expressed as a JSON file along with a set of sprites containing any raster artwork used for the shields. It can be generated as an object or hosted as a JSON file accessible by URL. See the next section for how to create this definition.
-
Hook up the shield generator to a maplibre-gl-js map. Pass either the URL of the JSON shield definition or create an object in javascript code. There are two separate classes for each approach.
new URLShieldRenderer("shields.json", routeExtractor)
.filterImageID(shieldPredicate)
.filterNetwork(networkPredicate)
.renderOn(map);
new ShieldRenderer(shields, shieldPredicate)
.filterImageID(shieldPredicate)
.filterNetwork(networkPredicate)
.renderOn(map);
Shield Definition
The purpose of the shield definition is to define which graphics and text to draw for each network/ref/name combination that you wish to display. This can be created in javascript as an object, or as an HTTP-accessible JSON file.
This description uses the following conventions:
- A network is a set of routes with a common graphical presentation. Each route may have variations in appearance, such as a different route number, or a special case definition as described below. The network value corresponds in concept to the OpenStreetMap network tag.
- A ref contains the text to be drawn on shield artwork. The ref value corresponds in concept to the OpenStreetMap ref tag.
The structure is as follows:
{
"options": {
"bannerTextColor": "black",
"bannerTextHaloColor": "white",
"shieldFont": "sans-serif-condensed, 'Arial Narrow', sans-serif"
},
"default": { ...definiton },
"network_1": { ...definiton },
"network_2": { ...definiton },
"network_2": { ...definiton }
}
The options block contains global parameters that apply across all shield drawing:
bannerTextColor
: color to draw text banners above the shieldbannerTextHaloColor
: color to draw an outline around the text bannershieldFont
: font to use for shield text and banners
You should create one definition entry for each network. The entry key must match the encoded network
value exactly. The "default" network defines what should be drawn if there's no definition for a particular network. A network definiton can contain any combination of the following parameters:
{
"textColor": "black",
"textHaloColor": "white",
"padding": {
"left": 3,
"right": 3,
"top": 3,
"bottom": 3
},
"spriteBlank": "name_of_image_1",
"noref": {
"spriteBlank": "name_of_image_2"
}
"canvasDrawnBlank": {
"drawFunc": "pentagon",
"params": {
"pointUp": false,
"offset": 5,
"angle": 0,
"fillColor": "white",
"strokeColor": "black",
"radius1": 2,
"radius2": 2
}
},
"modifiers": ["ALT"],
"textLayout": {
"constraintFunc": "roundedRect",
"options": {
"radius": 2
}
},
"colorLighten": "#006747",
"overrideByRef": {
"REF": {
"spriteBlank": "special_case_image",
"textColor": "#003f87",
"colorLighten": "#003f87"
}
},
"refsByWayName": {
"Audubon Parkway": "AU"
},
"overrideByWayName": {
"Merritt Parkway": {
"spriteBlank": "shield_us_ct_parkway_merritt"
}
}
}
Shield property descriptions
textColor
: determines what color to draw the ref
on the shield.textHaloColor
: color to draw a knockout halo around the ref
text.padding
: padding around the ref
, which allows you to squeeze the text into a smaller space within the shield.spriteBlank
: specify the name of an image in the sprite sheet to use as the shield background. This can either be a single string or an array of strings if there are multiple options for different width. If it's an array of strings, they must be ordered from narrowest to widest, and the engine will choose the narrowest shield graphic that fits the text at a reasonable size.noref
: specify alternate attributes to apply in the event that no ref
is supplied. This allows you to use one graphic for numbered routes and a separate unitary graphic for non-numbered routes within the same network. Supports spriteBlank
, colorLighten
, and colorDarken
.canvasDrawnBlank
: specify that a shield should be drawn as a common shape (rectangle, ellipse, pentagon, etc), with colors and dimensions as specified. See the drawn shield shapes section for available drawing options.modifiers
: specify that one or more short text strings (up to 4 characters) should be drawn above the shield. This is specified as an array, and text will be drawn in order from top to bottom.textLayout
: specify how text should be inscribed within the padded bounds of the shield. The text will be drawn at the maximum size allowed by this constraint. See the text layout functions section for text layout options.colorLighten
: specify that the shield artwork should be lightened (multiplied) by the specified color. This means that black areas will be recolor with this color and white areas will remain the same. Alpha values will remain unmodified.overrideByRef
: specify that a specific ref
within a network
should have different shield properties than other routes in the network, with one entry per special-case ref
. Supported options are spriteBlank
, textColor
, and colorLighten
.refsByWayName
: specify that a wayName
with the specified key should be treated as a ref
with the specified value.overrideWayName
: specify that particular wayName
should use a specific spriteBlank
which differs from the rest of the network.
Text layout functions
Text is laid out on shields in accordance with the specified textLayout
value. The text will be drawn, measured, and expanded until it hits the edge of a text layout constraint. For example, an ellipse
constraint would fill a padded shield like this, with the text drawn from the center and expanding until it reaches the ellipse:
Not all constraints are center-specified. For example, the southHalfEllipse
constraint would grow from the top of the shield as follows:
The supported text constraints are:
ellipse
rect
roundedRect
southHalfEllipse
Defining drawn shield shapes
If canvasDrawnBlank
is specified, the shield will be drawn as a shape. This needs to be specified with a drawing function, drawFunc
and a params
block the describes how the shape will be drawn. The draw functions are as follows:
| drawFunc |
---|
| diamond |
| ellipse |
| escutcheon |
| fishhead |
| hexagonVertical |
| hexagonHorizontal |
| octagonVertical |
| pentagon |
| roundedRectangle |
| trapezoid |
| triangle |
The following params
options can be specified:
angle
- applies to several shape types, indicates the key angle in degrees.fill
- specifies the internal fill color.outline
- specifies the outline color.outlineWidth
- specifies the width of the outline.pointUp
- applies to several shape types and specifies whether the pointy side is up.radius
- specifies the rounding radius, in pixels, to use for corners.radius1
- Corner radius of pointed side of pentagon (defaults to 2)radius2
- Corner radius of flat side of pentagon (defaults to 0)shortSideUp
- for trapezoid
only, a boolean which specifies whether the short side is up or down.
Custom shield graphics
In addition to the stock drawing functions, a custom draw function can be specified. paDot
and branson
are included as examples of this, for rendering the Allegheny County belt system and the Branson, Missouri colored route system. See the file src/custom_shields.mjs
for an example of how this is done.