This page aims to describe the general editing process of OpenStreetMap Carto, the style used for the Standard tile layer of OpenStreetMap, through some suggested practices.
The following workflow summarizes the main actions.
Check DB data.Within the PostGIS instance, find the DB columns related to the feature in scope.Verify that the needed column is included in openstreetmap-carto.style or that the hstore tags column can provide the needed feature through the tags->’feature’ syntax. As osm2pgsql populates hstore values for features that are not present in openstreetmap-carto.style, this file is expected to be rarely modified. |
Edit project.mml.Check whether the selected columns are already managed within the appropriate layers in project.mml.If everything is already appropriately defined, all modifications can be directly implemented within the CartoCSS .mss files.Conversely, if the feature is not present within the layer(s), project.mml has to be edited.For complex development, a new layer might be needed and in this case a new section has to be developed in project.mml. |
Edit the .mss style.The .mss files can then be modified to define the rendering attributes of the new feature within each related layer. CartoCSS selectors shall refer layers or classes defined in project.mml. Inside a selector, filters and properties define rendering attributes.If a new layer is added, possibly a new .mss stylesheet file needs to be created. |
Test modifications.All modifications must be tested (e.g., with Kosmtik) on different regions and using all zooms; regions shall be selected by analyzing wide areas, checking places with high and low concentration of the feature. |
Before entering into the details of editing the styles, in case you are also willing to contribute to OpenStreetMap Carto, you are suggested to have a look to Guidelines for adding new features. It’s a long thread which qualifies and limits the current contribution scope to this project.
The definition and configuration file of openstreetmap-carto is named project.mml and uses the YAML format. Wikipedia contains an introduction to YAML.
The reason for using the YAML format instead of the former JSON is described here and here:lit is easier to edit and maintain, especially for SQL queries. The current version of carto can directly process it.
The definition of project.mml and more CartoCSS stylesheets has been adoped by Mapbox basing on a convention from Cascadenik, a predecessor to CartoCSS created outside of Mapbox. In Cascadenik, project.mml contained XML with CSS-like stylesheet embedded in <Stylesheet><![CDATA[...]]></Stylesheet>
tag and, since the stylesheet included in project.mml started to grow, they moved it off to a separate file with MSS extension.1
The configuration of project.mml is grouped into sections, each configures a different aspect. Relevant sections:
In YAML, the order of the sections is not important. The indentation is significant and shall only contain space characters. Tabulators are not permitted for indentation. The order of each layer is important.
The first part relates to the globals settings:
scale: 1
metatile: 2
name: "OpenStreetMap Carto"
description: "A general-purpose OpenStreetMap mapnik style, in CartoCSS"
bounds: &world
- -180
- -85.05112877980659
- 180
- 85.05112877980659
center:
- 0
- 0
- 4
format: "png"
interactivity: false
minzoom: 0
maxzoom: 22
srs: "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over"
The above lines define the rendering settings for Mapnik. Notice that latitude bounds exclude the poles (with the same settings adopted by Google Maps), where the scale becomes infinite with Mercator projection.
The center tag defines the starting lat/long centre point (0, 0) and zoom (4); these correspond to the default map image shown by TileMill at startup.
srs is the adopted spatial reference system. The verbose +proj
definitions ensure Mapnik is programmed with all appropriate parameters (+over
for instance is required).2
The _parts
section defines the YAML aliases for the projection and for the datasource:
# Various parts to be included later on
_parts:
# Extents are used for tilemill, and don't actually make it to the generated XML
extents: &extents
extent: *world
srs-name: "900913"
srs: "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over"
extents84: &extents84
extent: *world
srs-name: "WGS84"
srs: "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
osm2pgsql: &osm2pgsql
type: "postgis"
dbname: "gis"
key_field: ""
geometry_field: "way"
extent: "-20037508,-20037508,20037508,20037508"
Description:
Pulling an example from the file, we get this reference YAML:
# Various parts to be included later on
_parts:
# Extents are used for tilemill, and don't actually make it to the generated XML
extents: &extents
extent: *world
srs-name: "900913"
srs: "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over"
osm2pgsql: &osm2pgsql
type: "postgis"
dbname: "gis"
key_field: ""
geometry_field: "way"
extent: "-20037508,-20037508,20037508,20037508"
Layer:
- id: "citywalls"
name: "citywalls"
class: ""
geometry: "linestring"
<<: *extents
Datasource:
<<: *osm2pgsql
table: |-
(SELECT
way
FROM planet_osm_line
WHERE historic = 'citywalls')
AS citywalls
advanced: {}
The <<: *extents
key merges the keys from the &extents
mapping of the _parts section into that location, avoiding having to specify them again, and same for <<: *osm2pgsql
. The idea is taken from the MapProxy documentation. Fortunately, an understanding of YAML isn’t needed to use them when adding a layer - you just copy from existing layers. The _parts
in JSON is ignored by carto and doesn’t impact the output XML; TileMill also appears to ignore it.3
The Stylesheet
section references all used .mss CartoCSS stylesheets to be integrated in the compiled Mapnik XML input file:
Stylesheet:
- "style.mss"
- "shapefiles.mss"
- "landcover.mss"
- "water.mss"
- "water-features.mss"
- "road-colors-generated.mss"
- "roads.mss"
- "power.mss"
- "placenames.mss"
- "buildings.mss"
- "stations.mss"
- "amenity-points.mss"
- "ferry-routes.mss"
- "aerialways.mss"
- "admin.mss"
- "addressing.mss"
Subsequently to the stylesheets to be included in the source file for Mapnik, all layers are defined.
Layer:
- ...
...
Layers are how sets of data are added to a map. Each layer references a single source file (e.g., shapefile) or database query (e.g., a PostGIS query). Multiple layers can be combined over top of each other to create the final map.4 A basic description of their definition is reported in the TimeMill documentation.
Layers are configured as arrays. The YAML format -
indicates the start of the next item in an array. Generally the -
appears before the id
attribute (but might happen before a different attribute, e.g., name
).5
Layers referring type: "postgis"
(e.g., <<: *osm2pgsql
in Datasource:
) use the Mapnik postgis plugin. Plugins read geometric data from some specific external format to the internal format used by Mapnik. The PostGIS plugin requires to specify a SQL query that returns a table of data. That table must contain a column called way (of PostGIS type geometry). The table may contain arbitrary other columns, which are used to support rendering and can be invoked in the styling rules. The SQL query will be augmented with a bounding box condition by Mapnik (ST_SetSRID(‘BOX3D(…). The SELECT needs a subquery specifying a SQL table alias, which is generally the same identifier used in the id
. In the above example, as the layer is named citywalls (- id: "citywalls"
), the query after table: |-
can be (but not necessarily is):
(SELECT way, ... FROM ... WHERE ...) AS citywalls
The Strip chomp modifier |-
(ref. YAML Reference card) is used to insert a block, preserving newlines and stripping the final line break character.
Some tokens can appear in a query processed by the PostGIS plugin of Mapnik: !bbox!
, !scale_denominator!
, !pixel_width!
and !pixel_height!
.
PostgreSQL queries which can be defined in project.mml might become complex and always need tuning. Check OptimizeRenderingWithPostGIS for details and analysis. Check also Identifying Slow Rendering Queries.
Relation between Geometry and SQL Table is the following:
Geometry | SQL Table | Type |
---|---|---|
“linestring” | planet_osm_line | way |
“linestring” | planet_osm_roads | way |
“point” | planet_osm_point | node |
“polygon” | planet_osm_polygon | relation |
project.mml at the moment provides 79 layers. Generally, the more features that your map will include, the more layers that you’ll want. Basing on the painter algorithm, the order in which they are defined is the order in which they are rendered.
Relevant attributes of each layer:
id
: layer identifier; defines a “#identifier” selector (selector preceded by hash), to be used in *.mss files.name
: the name of the layer, generally the same as id
; this references the layer and is used by programs like Tilemill, allowing the user to select layers and filter their visibility. Notice that carto 0.17 marks name
as deprecated and it will be removed in carto 1.06.class
: when set, defines one or more classes, to be used in mss files as *.identifier selector (selector preceded by dot); classes are used to define a single rendering description for many layers sharing the same class name. For instance, class: "barriers"
is used by id: "area-barriers"
and by id: "line-barriers"
; then, in landcover.mss, a selector named .barriers {...}
defines attributes which are common to both layers.geometry
: symbolizer type which can be linestring
(way), point
(node) or polygon
. These attributes can be used as filters.Datasource
: can be file: "filename"
/type: "shape"
for shapefiles, or <<: *osm2pgsql
/table: |-
for PostGIS queries.properties
might indicate minzoom
and maxzoom
, which filter the Mapnik rendering to the defined set of zoom levels (also used to improve rendering performance, by avoiding to process layers which are only filtered within specific zooms in their .mss stylesheets).advanced
is not valued in openstreetmap-carto (e.g., advanced: {}
)..mss stylesheets are in CartoCSS format.
The CartoCSS reference manual (by Mapbox) can be found in the Carto documentation.
A description of efficient methods for structuring CartoCSS styles is available at CartoCSS Best Practices.
A critical review of CartoCSS is reported in The end of CartoCSS blog post.
The columns in the SQL queries define the CartoCSS properties used as filter selectors or labels.
Consider the following style defined in project.mml (with some revisions as example):
- id: "water-areas"
name: "water-areas"
class: "water-elements"
geometry: "polygon"
<<: *extents
Datasource:
<<: *osm2pgsql
table: |-
(SELECT
way,
"natural",
waterway,
landuse,
name,
way_area/NULLIF(!pixel_width!::real*!pixel_height!::real,0) AS way_pixels
FROM planet_osm_polygon
WHERE
(waterway IN ('dock', 'riverbank', 'canal')
OR landuse IN ('reservoir', 'basin')
OR "natural" IN ('water', 'glacier'))
AND building IS NULL
AND way_area > 0.01*!pixel_width!::real*!pixel_height!::real
ORDER BY z_order, way_area DESC
) AS water_areas
properties:
minzoom: 4
advanced: {}
In the above example, the style is identified as “water-areas”, named with the same label, rendered at zoom >= 4; it uses polygons, defines a class named “water-elements” and provides the following properties for the CartoCSS stylesheet: [name]
, [landuse]
, [waterway]
, [way_pixels]
(the latter produces the area in screen pixels), [natural]
(in double quotes to prevent being interpreted as SQL token); these are the columns of the SQL Query.
Notice that the area is calculated in pixels and not in meters, to avoid the need of compensation at different latitudes in relation to the Mercator projection. Similarly, a line length should be calculated in pixels using the geometric mean of x and y (sqrt(x*y))
7:
ST_Length(way)/NULLIF(SQRT(!pixel_width!::real*!pixel_height!::real),0)
A file named water.mss includes the related CartoCSS styles.
It might have a selector named #water-areas
to specifically refer this layer:
#water-areas {
# styles will apply to 'water-areas' layer only
...
}
Or it might have the .water-elements
class:
.water-elements {
# this applies to all layers with class 'water-elements'
...
}
SQL statements might also include ordering by user-supplied strings in the last term of an ORDER BY clause to get a consistent ordering across metatiles.
Let us consider this simplified CartoCSS example:
#water-areas {
[natural = 'glacier']::natural {
[zoom >= 6] {
line-width: 0.75;
line-color: @glacier-line;
polygon-fill: @glacier;
[zoom >= 8] {
line-width: 1.0;
}
[zoom >= 10] {
...
}
}
}
[waterway = 'dock'],
[waterway = 'canal'] {
text-name: "[name]";
...
}
[landuse = 'basin'][zoom >= 7]::landuse {
polygon-fill: @water-color;
[way_pixels >= 4] {
polygon-gamma: 0.75;
}
[way_pixels >= 64] {
polygon-gamma: 0.6;
}
}
...
}
In the above example, landuse, waterway, way_pixels and natural are used as filters, while name is used as label. All are DB columns.
A feature might be rendered through more layers. Generally (but not always) a layer is rendered through a specific stylesheet (.mss).
Take for instance amenity=place_of_worship
, which is defined in the following layers (within the current version of project.mml):
#landcover
selector in landcover.mss#buildings-major
selector in buildings.mss.points
class in amenity-points.mss.points
class in amenity-points.mss (uses the same class of amenity-points-poly).text
class in amenity-points.mssStylesheet landcover.mss:
...
#landcover-low-zoom[zoom < 10],
#landcover[zoom >= 10] {
...
[feature = 'amenity_place_of_worship'][zoom >= 13] {
... # fill polygon
}
...
}
...
Stylesheet buildings.mss:
...
#buildings-major {
[zoom >= 13] {
...
[amenity = 'place_of_worship'] {
... # special highlights for polygons with wide "way_area"
}
}
}
...
Stylesheet amenity-points.mss:
...
.points {
...
[feature = 'amenity_place_of_worship'][zoom >= 16] {
... # add marker
}
...
}
...
.text-low-zoom[zoom < 10],
.text[zoom >= 10] {
...
[feature = 'amenity_place_of_worship'][zoom >= 17] {
... # add text
}
...
}
...
The reference document is CARTOGRAPHY. It is an important prerequisite to start contributing to the Openstreetmap Carto style.
The reference document is CONTRIBUTING. This document includes essential development guidelines.
A file named USECASES describes which features should be rendered on a given zoomlevel and for a specific use case. This report is currently restricted to some low zoom levels (5, 6 and 7).
All colours used in openstreetmap-carto are defined in RGB, with annotations in LCh. This makes it much easier to review different colours comparing their related LCh parameters. An example is the need to define a number of tones at the same saturation/chroma and lightness but with different hues. When you do this in LCh, simply the hue parameter can be changed, keeping the other two values unaltered.8
LCh is a perceptually uniform reference color space based on actual studies of how people perceive colors. The LCh color wheel has four “primary” colors, yellow, cyan, violet-blue, and magenta-red. In this color space, L indicates lightness, C represents chroma or relative saturation, and h is the hue angle in polar coordinates. The value of chroma C is the distance from the lightness axis (L) and starts at 0 in the center. Hue angle is expressed in degrees ranging from 0 to 360 (e.g., 0° is red, and 90° is or yellow, 360° is red). It uses cylindrical coordinates. Lightness ranges from 0 to 100; dark to bright. Chroma ranges from 0 to 100 too, unsaturated to fully saturated.
All colours in openstreetmap-carto are anyway just annotated in LCh and reported in RGB as this is the color space used by Mapnik, to avoid gamut outside of RGB color space9. The conversion between LCh and RGB shall be done manually with an external program. As LCh has the problem that it is possible to specify colours that cannot be represented in RGB, a preliminarily defined LCh color has to be manually adjusted so that the corresponding RGB value exists.
Christoph Hormann published an excellent document on Design goals and guidelines for the Openstreetmap-carto style. It extends the official CARTOGRAPHY document with considerations on colors and zoom levels; reading and understanding it is recommended.
Check also the various notes within the Imagico’s blog.
When defining the appropriate zoom level to render a feature, it is important to consider the effect of the Mercator projection.
Because Mercator is a variable scale projection, there is no direct relationship between zoom level and scale. At a typical resolution computer screen, z13 for example can be somewhere between about 1:70000 (Equator) and 1:8000 (northern Greenland).
A given scale for equator can be adjusted to a specific latitude by multiplying it by cos(latitude). For example, divide by 2 for latitude 60 (Oslo, Helsinki, Saint-Petersburg).
Resolution and Scale of Slippy map tiles is described here and here.
Some features need to scale up their font size according to the zoom level. If you are implementing different text sizes according to the zoom, consider also different wordwrap. The pattern to follow for the text size logic is described in Multi-line labels in CONTRIBUTING, landuse samples and lakes.
text-allow-overlap: true
which make characters overlap (not allowed in cartography).The following scripts are provided to support coding. They are necessary and useful solely during making changes to the map style and are unnecessary for map rendering.
This script generates and populates the data directory with all needed shapefiles, including indexing them through shapeindex. Check INSTALL for further documentation.
The shapefile configuration is read from a dictionary file which is by default external-data.yml, that describes the datasets as well as related download and installation process as declarative workflow.
At list 24 GB HD and 4 GB RAM are suggested to successfully run this procedure.
usage: get-external-data.py [-h] [-f] [-c CONFIG] [-D DATA] [-d DATABASE] [-H HOST] [-p PORT] [-U USERNAME] [-v] [-q] [-w PASSWORD] [-R RENDERUSER]
Load external data into a database
optional arguments:
-h, --help show this help message and exit
-f, --force Download new data, even if not required
-c CONFIG, --config CONFIG
Name of configuration file (default external-data.yml)
-D DATA, --data DATA Override data download directory
-d DATABASE, --database DATABASE
Override database name to connect to
-H HOST, --host HOST Override database server host or socket directory
-p PORT, --port PORT Override database server port
-U USERNAME, --username USERNAME
Override database user name
-v, --verbose Be more verbose. Overrides -q
-q, --quiet Only report serious problems
-w PASSWORD, --password PASSWORD
Override database password
-R RENDERUSER, --renderuser RENDERUSER
User to grant access for rendering
get-external-data.py can be run from the scripts directory of openstreetmap-carto, or from its base folder.
Typical usage:
scripts/get-external-data.py
For any modification on the road classes colours, do not modify road-colors-generated.mss directly. Check instead road-colors.yaml and related internal description. Then run scripts/generate_road_colours.py > road-colors-generated.mss
.
usage: generate_road_colours.py [-h] [-v]
Generates road colours
optional arguments:
-h, --help show this help message and exit
-v, --verbose Generates information about colour differences
Typical usage:
$ pip install colormath # (or "sudo pip install colormath" to install the needed Python prerequisite)
$ scripts/generate_road_colours.py > road-colors-generated.mss
Not all values are possible; generate_road_colours.py will throw an error if you pick values that cannot be converted to RGB. Usage of HUSL is recommended.
This script generates all SVG files inside the symbols/shields folder related to highway shields. It uses road-colors.yaml to configure the shield colors.
The generated files are then used by roads.mss (specifically, by roads-text-ref-low-zoom and roads-text-ref styles).
Files are named symbols/shields/[highway]_[width]x[height].svg, symbols/shields/[highway]_[width]x[height]_z16.svg and symbols/shields/[highway]_[width]x[height]_z18.svg where width = 1 to 10 and height = 1 to 4.
Related configuration is internal. The script exploits generate_road_colours.py to read road-colors.yaml configuration file.
The currently produced files are related to motorway, trunk, primary, secondary and tertiary highway tags (while track and path are not managed at the moment).
Installation
Installation of prerequisite components with Windows:
Download the LXML WHL library from https://pypi.python.org/pypi/lxml
Example using Python 2.7:
> pip install lxml-3.6.4-cp27-cp27m-win32.whl
> pip install colormath
Example using Python 3.5:
> pip install lxml-3.6.4-cp35-cp35m-win32.whl
> pip install colormath
Running the script:
> scripts/generate_road_colours.py > road-colors-generated.mss
> scripts/generate_shields.py
Installation of prerequisite components with Ubuntu:
sudo apt-get install -y python-pip python3-pip libxml2-dev libxslt1-dev python-dev python-lxml python-colormath
sudo pip install colormath
sudo pip install lxml
Running the script:
$ scripts/generate_road_colours.py > road-colors-generated.mss
$ scripts/generate_shields.py
This Ruby script generates a list of popular shop values with more than MIN_COUNT occurences in OpenStreetMap database according to taginfo. it is useful during creating/updating list of shops displayed with generic dot icon.
A new SQL query defined in project.mml might need the definition of SQL indexes that shall be coded in indexes.yml, which is the related dictionary adopted by OpenStreetMap Carto.
Do not modify indexes.sql directly: use this script instead, which reads indexes.yml and creates SQL statements to the standard output, to be redirected to indexes.sql. There are a number of options for concurrent index creation, recreating the osm2pgsql-built indexes, fillfactors, and other settings to give full control of the resulting statements.
usage: indexes.py [-h] [--concurrent] [--fillfactor FILLFACTOR] [--notexist]
[--osm2pgsql] [--reindex]
Generates custom index statements
optional arguments:
-h, --help show this help message and exit
--concurrent Generate indexes CONCURRENTLY
--fillfactor FILLFACTOR
Custom fillfactor to use
--notexist Use IF NOT EXISTS (requires 9.5)
--osm2pgsql Include indexes normally built by osm2pgsql
--reindex Rebuild existing indexes
Typical usage:
$ scripts/index.py > index.sql
A goal with the indexes is to have them general-purpose enough to not need frequent changing with stylesheet changes, but to be usable with many versions, and potentially other styles.
Validate the MML against multiple Mapnik versions, and report its lines for debugging purposes:
sudo apt install libxml2-utils
for m in 3.0.0 3.0.12; do carto -a $m project.mml 2>&- | xmllint - | wc -l; done
Validate that the SVGs are valid XML:
find symbols/ -name '*.svg' | xargs xmllint --noout
Check and validate the Lua tag transforms:
sudo apt install lua5.2 # install Lua interpreter (osm2pgsql embeds it)
cd openstreetmap-carto # position inside the openstreetmap-carto directory
lua scripts/lua/test.lua # run the test script
Within openstreetmap-carto project, folder with pathname symbols/generating_patterns includes sources (.svg files), process description (.md files) and produced images (.png files) of patterns built from two separately generated svg files by means of raster processing.
Details are in generating_patterns folder, which also includes *.md (markdown) documents.
OpenStreetMap uses a topological data structure with four core elements (also known as data primitives):
A recommended ontology of map features (the meaning of tags) is maintained on the OSM Wiki.
Text taken from gravitystorm’s comment for pull 2473 ↩
Text taken from an openstreetmap-carto comment for issue 2101. ↩
Text taken from an openstreetmap-carto comment for pull 947. ↩
Text taken from TileMill documentation ↩
Text taken from an openstreetmap-carto comment for issue 528. ↩
Text taken from nebulon42’s comment for pull 2506 ↩
Text taken from an openstreetmap-carto comment for pull 2138 ↩
Text taken from imagico’s comment for pull 2138 ↩