diff --git a/.travis.yml b/.travis.yml index 3426d0277c..515600f286 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,10 @@ language: node_js sudo: false node_js: - "0.10" +addons: + apt: + packages: + - lua5.1 env: - CARTO=0.16.0 MAPNIK='3.0.0 3.0.12' - CARTO=0.16.3 MAPNIK='3.0.0 3.0.12' @@ -16,3 +20,5 @@ script: - for m in $MAPNIK; do ./node_modules/carto/bin/carto -a $m project.mml | xmllint - | wc -l; done # Validate that the SVGs are valid XML - find symbols/ -name '*.svg' | xargs xmllint --noout + # Check the Lua transforms + - lua scripts/lua/test.lua diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c9555fdced..72c065ba41 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,16 @@ contain a cropped screenshot of the problem, and a link to the area. Don't assum that we will see exactly what you see. If a particular OSM object is an issue, the issue should contain the tagging of the object. +## 3.x compatibility + +OpenStreetMap Carto is currently maintaining compatibility in the output of 3.x +and 4.x to allow users to smoothly transition and reload their databases. Pull +requests which cannot be backported to 3.x are not being accepted at this time +and will be closed. + +Whenever a pull request is merged into master, it also needs to be backported to +the 3.x branch at the same time by the person merging. + ## Easy pickings Some [easy issues](https://github.com/gravitystorm/openstreetmap-carto/issues?q=is%3Aopen+is%3Aissue+label%3Aeasy) have been selected @@ -115,6 +125,8 @@ Because SQL within JSON or YAML will not generally be syntax highlighted, indent * Add indentation if necessary for complex function calls, WHERE parenthesis, and CASE statements * One space before and after = etc * Name SQL subqueries after the layer name (but use underscores) +* When extracting tags from hstore, use `tags->'foo'`, not `tags -> 'foo'`, and only add parenthesis if needed for order of operations +* To check if a tag is in the tags hstore, use `tags @> 'foo=>bar'`, relying on automatic conversion from `text` to `hstore`. ## Map Icon Guidelines diff --git a/INSTALL.md b/INSTALL.md index c9e1803db4..c2987e21aa 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -3,10 +3,10 @@ ## OpenStreetMap data You need OpenStreetMap data loaded into a PostGIS database (see below for [dependencies](#dependencies)). These stylesheets currently work only with the osm2pgsql defaults (i.e. database name is `gis`, table names are `planet_osm_point`, etc). -It's probably easiest to grab an PBF of OSM data from [Mapzen](https://mapzen.com/metro-extracts/) or [geofabrik](http://download.geofabrik.de/). Once you've set up your PostGIS database, import with osm2pgsql: +Start by setting up your database to have PostGIS and hstore with ``psql -d gis -c 'CREATE EXTENSION postgis; CREATE EXTENSION hstore;'``, then grab some OSM data. It's probably easiest to grab an PBF of OSM data from [Mapzen](https://mapzen.com/metro-extracts/) or [geofabrik](http://download.geofabrik.de/). Once you've done that, import with osm2pgsql: ``` -osm2pgsql -d gis ~/path/to/data.osm.pbf --style openstreetmap-carto.style +osm2pgsql -G --hstore --style openstreetmap-carto.style --tag-transform-script openstreetmap-carto.lua -d gis ~/path/to/data.osm.pbf ``` You can find a more detailed guide to setting up a database and loading data with osm2pgsql at [switch2osm.org](http://switch2osm.org/loading-osm-data/). diff --git a/README.md b/README.md index 82d0a2994e..361da6216e 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,9 @@ and issues have their own [tag](https://github.com/gravitystorm/openstreetmap-ca Initial releases will not make use of the new features, maintaining compatibility with v3.x and v3.x releases will continue with backports, allowing the style to be -rendered from either a new database or an old one. - -When sufficient time has passed for users to reload their databases, new features -can be used and 3.x compatibility will not be maintained. +rendered from either a new database or an old one. In order to allow time for users +to reload their databases, this will be maintained until at least two MINOR +releases have occurred. After that compatibility will not be maintained. # Alternatives diff --git a/openstreetmap-carto.lua b/openstreetmap-carto.lua new file mode 100644 index 0000000000..26117e3042 --- /dev/null +++ b/openstreetmap-carto.lua @@ -0,0 +1,422 @@ +-- For documentation of Lua tag transformations, see: +-- https://github.com/openstreetmap/osm2pgsql/blob/master/docs/lua.md + +-- Objects with any of the following keys will be treated as polygon +local polygon_keys = { + 'abandoned:aeroway', + 'abandoned:amenity', + 'abandoned:building', + 'abandoned:landuse', + 'abandoned:power', + 'aeroway', + 'amenity', + 'area:highway', + 'building', + 'building:part', + 'harbour', + 'historic', + 'landuse', + 'leisure', + 'man_made', + 'military', + 'natural', + 'office', + 'place', + 'power', + 'public_transport', + 'shop', + 'tourism', + 'water', + 'waterway', + 'wetland' +} + +-- Objects with any of the following key/value combinations will be treated as linestring +local linestring_values = { + leisure = {track = true, slipway = true}, + man_made = {embankment = true, breakwater = true, groyne = true}, + natural = {cliff = true, tree_row = true}, + historic = {citywalls = true}, + waterway = {canal = true, derelict_canal = true, ditch = true, drain = true, river = true, stream = true, wadi = true, weir = true}, + power = {line = true, minor_line = true}, + natural = {ridge = true, arete = true} +} + +-- Objects with any of the following key/value combinations will be treated as polygon +local polygon_values = { + highway = {services = true, rest_area = true}, + junction = {yes = true} +} + +-- The following keys will be deleted +local delete_tags = { + 'note', + 'source', + 'source_ref', + 'attribution', + 'comment', + 'fixme', + -- Tags generally dropped by editors, not otherwise covered + 'created_by', + 'odbl', + 'odbl:note', + -- Lots of import tags + -- EUROSHA (Various countries) + 'project:eurosha_2012', + + -- UrbIS (Brussels, BE) + 'ref:UrbIS', + + -- NHN (CA) + 'accuracy:meters', + 'sub_sea:type', + 'waterway:type', + -- StatsCan (CA) + 'statscan:rbuid', + + -- RUIAN (CZ) + 'ref:ruian:addr', + 'ref:ruian', + 'building:ruian:type', + -- DIBAVOD (CZ) + 'dibavod:id', + -- UIR-ADR (CZ) + 'uir_adr:ADRESA_KOD', + + -- GST (DK) + 'gst:feat_id', + + -- Maa-amet (EE) + 'maaamet:ETAK', + -- FANTOIR (FR) + 'ref:FR:FANTOIR', + + -- 3dshapes (NL) + '3dshapes:ggmodelk', + -- AND (NL) + 'AND_nosr_r', + + -- OPPDATERIN (NO) + 'OPPDATERIN', + -- Various imports (PL) + 'addr:city:simc', + 'addr:street:sym_ul', + 'building:usage:pl', + 'building:use:pl', + -- TERYT (PL) + 'teryt:simc', + + -- RABA (SK) + 'raba:id', + -- DCGIS (Washington DC, US) + 'dcgis:gis_id', + -- Building Identification Number (New York, US) + 'nycdoitt:bin', + -- Chicago Building Inport (US) + 'chicago:building_id', + -- Louisville, Kentucky/Building Outlines Import (US) + 'lojic:bgnum', + -- MassGIS (Massachusetts, US) + 'massgis:way_id', + + -- misc + 'import', + 'import_uuid', + 'OBJTYPE', + 'SK53_bulk:load' +} +delete_prefixes = { + 'note:', + 'source:', + -- Corine (CLC) (Europe) + 'CLC:', + + -- Geobase (CA) + 'geobase:', + -- CanVec (CA) + 'canvec:', + -- Geobase (CA) + 'geobase:', + + -- osak (DK) + 'osak:', + -- kms (DK) + 'kms:', + + -- ngbe (ES) + -- See also note:es and source:file above + 'ngbe:', + + -- Friuli Venezia Giulia (IT) + 'it:fvg:', + + -- KSJ2 (JA) + -- See also note:ja and source_ref above + 'KSJ2:', + -- Yahoo/ALPS (JA) + 'yh:', + + -- LINZ (NZ) + 'LINZ2OSM:', + 'linz2osm:', + 'LINZ:', + + -- WroclawGIS (PL) + 'WroclawGIS:', + -- Naptan (UK) + 'naptan:', + + -- TIGER (US) + 'tiger:', + -- GNIS (US) + 'gnis:', + -- National Hydrography Dataset (US) + 'NHD:', + 'nhd:', + -- mvdgis (Montevideo, UY) + 'mvdgis:' +} + +-- Big table for z_order and roads status for certain tags. z=0 is turned into +-- nil by the z_order function +local roads_info = { + highway = { + motorway = {z = 380, roads = true}, + trunk = {z = 370, roads = true}, + primary = {z = 360, roads = true}, + secondary = {z = 350, roads = true}, + tertiary = {z = 340, roads = false}, + residential = {z = 330, roads = false}, + unclassified = {z = 330, roads = false}, + road = {z = 330, roads = false}, + living_street = {z = 320, roads = false}, + pedestrian = {z = 310, roads = false}, + raceway = {z = 300, roads = false}, + motorway_link = {z = 240, roads = true}, + trunk_link = {z = 230, roads = true}, + primary_link = {z = 220, roads = true}, + secondary_link = {z = 210, roads = true}, + tertiary_link = {z = 200, roads = false}, + service = {z = 150, roads = false}, + track = {z = 110, roads = false}, + path = {z = 100, roads = false}, + footway = {z = 100, roads = false}, + bridleway = {z = 100, roads = false}, + cycleway = {z = 100, roads = false}, + steps = {z = 90, roads = false}, + platform = {z = 90, roads = false}, + construction = {z = 10, roads = false} + }, + railway = { + rail = {z = 440, roads = true}, + subway = {z = 420, roads = true}, + narrow_gauge = {z = 420, roads = true}, + light_rail = {z = 420, roads = true}, + funicular = {z = 420, roads = true}, + preserved = {z = 420, roads = false}, + monorail = {z = 420, roads = false}, + miniature = {z = 420, roads = false}, + turntable = {z = 420, roads = false}, + tram = {z = 410, roads = false}, + disused = {z = 400, roads = false}, + construction = {z = 400, roads = false}, + platform = {z = 90, roads = false}, + }, + aeroway = { + runway = {z = 60, roads = false}, + taxiway = {z = 50, roads = false}, + }, + boundary = { + administrative = {z = 0, roads = true} + }, +} + +local excluded_railway_service = { + spur = true, + siding = true, + yard = true +} +--- Gets the z_order for a set of tags +-- @param tags OSM tags +-- @return z_order if an object with z_order, otherwise nil +function z_order(tags) + local z = 0 + for k, v in pairs(tags) do + if roads_info[k] and roads_info[k][v] then + z = math.max(z, roads_info[k][v].z) + end + end + return z ~= 0 and z or nil +end + +--- Gets the roads table status for a set of tags +-- @param tags OSM tags +-- @return 1 if it belongs in the roads table, 0 otherwise +function roads(tags) + for k, v in pairs(tags) do + if roads_info[k] and roads_info[k][v] and roads_info[k][v].roads then + if not (k ~= 'railway' or tags.service) then + return 1 + elseif not excluded_railway_service[tags.service] then + return 1 + end + end + end + return 0 +end + +--- Generic filtering of OSM tags +-- @param tags Raw OSM tags +-- @return Filtered OSM tags +function filter_tags_generic(tags) + -- Short-circuit for untagged objects + if next(tags) == nil then + return 1, {} + end + + -- Delete tags listed in delete_tags + for _, d in ipairs(delete_tags) do + tags[d] = nil + end + + -- By using a second loop for wildcards we avoid checking already deleted tags + for tag, _ in pairs (tags) do + for _, d in ipairs(delete_prefixes) do + if string.sub(tag, 1, string.len(d)) == d then + tags[tag] = nil + break + end + end + end + + -- Filter out objects that have no tags after deleting + if next(tags) == nil then + return 1, {} + end + + -- Convert layer to an integer + tags['layer'] = layer(tags['layer']) + return 0, tags +end + +-- Filtering on nodes +function filter_tags_node (keyvalues, numberofkeys) + return filter_tags_generic(keyvalues) +end + +-- Filtering on relations +function filter_basic_tags_rel (keyvalues, numberofkeys) + -- Filter out objects that are filtered out by filter_tags_generic + local filter, keyvalues = filter_tags_generic(keyvalues) + if filter == 1 then + return 1, keyvalues + end + + -- Filter out all relations except route, multipolygon and boundary relations + if ((keyvalues["type"] ~= "route") and (keyvalues["type"] ~= "multipolygon") and (keyvalues["type"] ~= "boundary")) then + return 1, keyvalues + end + + return 0, keyvalues +end + +-- Filtering on ways +function filter_tags_way (keyvalues, numberofkeys) + local filter = 0 -- Will object be filtered out? + local polygon = 0 -- Will object be treated as polygon? + + -- Filter out objects that are filtered out by filter_tags_generic + filter, keyvalues = filter_tags_generic(keyvalues) + if filter == 1 then + return filter, keyvalues, polygon, roads + end + + polygon = isarea(keyvalues) + + -- Add z_order column + keyvalues["z_order"] = z_order(keyvalues) + + return filter, keyvalues, polygon, roads(keyvalues) +end + +--- Handling for relation members and multipolygon generation +-- @param keyvalues OSM tags, after processing by relation transform +-- @param keyvaluemembers OSM tags of relation members, after processing by way transform +-- @param roles OSM roles of relation members +-- @param membercount number of members +-- @return filter, cols, member_superseded, boundary, polygon, roads +function filter_tags_relation_member (keyvalues, keyvaluemembers, roles, membercount) + local members_superseded = {} + + -- Start by assuming that this not an old-style MP + for i = 1, membercount do + members_superseded[i] = 0 + end + + local type = keyvalues["type"] + + -- Remove type key + keyvalues["type"] = nil + + -- Filter out relations with just a type tag or no tags + if next(keyvalues) == nil then + return 1, keyvalues, members_superseded, 0, 0, 0 + end + + if type == "boundary" or (type == "multipolygon" and keyvalues["boundary"]) then + keyvalues.z_order = z_order(keyvalues) + return 0, keyvalues, members_superseded, 1, 0, roads(keyvalues) + -- For multipolygons... + elseif (type == "multipolygon") then + -- Multipolygons by definition are polygons, so we know roads = linestring = 0, polygon = 1 + keyvalues.z_order = z_order(keyvalues) + return 0, keyvalues, members_superseded, 0, 1, 0 + elseif type == "route" then + keyvalues.z_order = z_order(keyvalues) + return 0, keyvalues, members_superseded, 1, 0, roads(keyvalues) + end + + -- Unknown type of relation or no type tag + return 1, keyvalues, members_superseded, 0, 0, 0 +end + +--- Check if an object with given tags should be treated as polygon +-- @param tags OSM tags +-- @return 1 if area, 0 if linear +function isarea (tags) + -- Treat objects tagged as area=yes polygon, other area as no + if tags["area"] then + return tags["area"] == "yes" and 1 or 0 + end + + -- Search through object's tags + for k, v in pairs(tags) do + -- Check if it has a polygon key and not a linestring override, or a polygon k=v + for _, ptag in ipairs(polygon_keys) do + if k == ptag and not (linestring_values[k] and linestring_values[k][v]) then + return 1 + end + end + + if (polygon_values[k] and polygon_values[k][v]) then + return 1 + end + end + return 0 +end + +function is_in (needle, haystack) + for index, value in ipairs (haystack) do + if value == needle then + return true + end + end + return false +end + +--- Normalizes layer tags +-- @param v The layer tag value +-- @return An integer for the layer tag +function layer (v) + return v and string.find(v, "^-?%d+$") and tonumber(v) < 100 and tonumber(v) > -100 and v or nil +end diff --git a/openstreetmap-carto.style b/openstreetmap-carto.style index d3a005ce85..77134cb705 100644 --- a/openstreetmap-carto.style +++ b/openstreetmap-carto.style @@ -1,154 +1,55 @@ -# This is the .style file for OpenStreetMap Carto, which is currently -# the same as the upstream osm2pgsql style -# phstore is used instead of polygon,nocolumn to preserve compatibility -# with older osm2pgsql versions +# This is the osm2pgsql .style file for openstreetmap-carto. +# It is inteded to be used with openstreetmap-carto.lua and osm2pgsql Lua +# transforms. Full usage details are in INSTALL.md +# Among things, this means that the linear vs polygon distinction in this file +# doesn't matter, because that is set in the Lua and this file is only used for +# column names and types. # OsmType Tag DataType Flags node,way access text linear node,way addr:housename text linear node,way addr:housenumber text linear -node,way addr:interpolation text linear +way addr:interpolation text linear node,way admin_level text linear node,way aerialway text linear node,way aeroway text polygon node,way amenity text polygon -node,way area text polygon # hard coded support for area=1/yes => polygon is in osm2pgsql node,way barrier text linear -node,way bicycle text linear -node,way brand text linear -node,way bridge text linear +way bicycle text linear +way bridge text linear node,way boundary text linear node,way building text polygon -node capital text linear -node,way construction text linear -node,way covered text linear -node,way culvert text linear -node,way cutting text linear -node,way denomination text linear -node,way disused text linear -node ele text linear -node,way embankment text linear -node,way foot text linear -node,way generator:source text linear -node,way harbour text polygon +way construction text linear +way covered text linear +way foot text linear node,way highway text linear node,way historic text polygon -node,way horse text linear -node,way intermittent text linear +way horse text linear node,way junction text linear node,way landuse text polygon -node,way layer text linear +node,way layer int4 linear node,way leisure text polygon node,way lock text linear node,way man_made text polygon node,way military text polygon -node,way motorcar text linear node,way name text linear -node,way natural text polygon # natural=coastline tags are discarded by a hard coded rule in osm2pgsql -node,way office text polygon +node,way natural text polygon node,way oneway text linear -node,way operator text linear node,way place text polygon -node,way population text linear node,way power text polygon -node,way power_source text linear -node,way public_transport text polygon node,way railway text linear node,way ref text linear node,way religion text linear -node,way route text linear -node,way service text linear +way route text linear +way service text linear node,way shop text polygon -node,way sport text polygon -node,way surface text linear -node,way toll text linear +way surface text linear node,way tourism text polygon -node,way tower:type text linear way tracktype text linear -node,way tunnel text linear +way tunnel text linear node,way water text polygon node,way waterway text polygon -node,way wetland text polygon -node,way width text linear -node,way wood text linear -node,way z_order int4 linear # This is calculated during import way way_area real linear # This is calculated during import -# Area tags -# We don't make columns for these tags, but objects with them are areas. -way abandoned:aeroway text phstore -way abandoned:amenity text phstore -way abandoned:building text phstore -way abandoned:landuse text phstore -way abandoned:power text phstore -way area:highway text phstore - -# Deleted tags -# These are tags that are generally regarded as useless for most rendering. -# Most of them are from imports or intended as internal information for mappers -# Some of them are automatically deleted by editors. -# If you want some of them, perhaps for a debugging layer, just delete the lines. - -# These tags are used by mappers to keep track of data. -# They aren't very useful for rendering. -node,way note text delete -node,way note:* text delete -node,way source text delete -node,way source_ref text delete -node,way source:* text delete -node,way attribution text delete -node,way comment text delete -node,way fixme text delete - -# Tags generally dropped by editors, not otherwise covered -node,way created_by text delete -node,way odbl text delete -node,way odbl:note text delete -node,way SK53_bulk:load text delete - -# Lots of import tags -# TIGER (US) -node,way tiger:* text delete - -# NHD (US) -# NHD has been converted every way imaginable -node,way NHD:* text delete -node,way nhd:* text delete - -# GNIS (US) -node,way gnis:* text delete - -# Geobase (CA) -node,way geobase:* text delete -# NHN (CA) -node,way accuracy:meters text delete -node,way sub_sea:type text delete -node,way waterway:type text delete - -# KSJ2 (JA) -# See also note:ja and source_ref above -node,way KSJ2:* text delete -# Yahoo/ALPS (JA) -node,way yh:* text delete - -# osak (DK) -node,way osak:* text delete - -# kms (DK) -node,way kms:* text delete - -# ngbe (ES) -# See also note:es and source:file above -node,way ngbe:* text delete - -# naptan (UK) -node,way naptan:* text delete - -# Corine (CLC) (Europe) -node,way CLC:* text delete - -# misc -node,way 3dshapes:ggmodelk text delete -node,way AND_nosr_r text delete -node,way import text delete -node,way it:fvg:* text delete +# Columns defined in openstreetmap-carto.lua file +way z_order int4 linear diff --git a/project.mml b/project.mml index 0a3a3dc964..c6746afdc6 100644 --- a/project.mml +++ b/project.mml @@ -107,14 +107,14 @@ Layer: way, COALESCE(name, '') AS name, ('landuse_' || (CASE WHEN landuse IN ('forest', 'military') THEN landuse ELSE NULL END)) AS landuse, ('natural_' || (CASE WHEN "natural" IN ('wood', 'sand', 'scree', 'shingle', 'bare_rock') THEN "natural" ELSE NULL END)) AS "natural", - ('wetland_' || (CASE WHEN "natural" IN ('wetland', 'mud') THEN (CASE WHEN "natural" IN ('mud') THEN "natural" ELSE wetland END) ELSE NULL END)) AS wetland, + ('wetland_' || (CASE WHEN "natural" IN ('wetland', 'mud') THEN (CASE WHEN "natural" IN ('mud') THEN "natural" ELSE tags->'wetland' END) ELSE NULL END)) AS wetland, way_area/NULLIF(!pixel_width!::real*!pixel_height!::real,0) AS way_pixels FROM planet_osm_polygon WHERE (landuse IN ('forest', 'military') OR "natural" IN ('wood', 'wetland', 'mud', 'sand', 'scree', 'shingle', 'bare_rock')) AND way_area > 0.01*!pixel_width!::real*!pixel_height!::real AND building IS NULL - ORDER BY CASE WHEN layer~E'^-?\\d+$' AND length(layer)<10 THEN layer::integer ELSE 0 END, way_area DESC + ORDER BY COALESCE(layer,0), way_area DESC ) AS features ) AS landcover_low_zoom properties: @@ -145,7 +145,7 @@ Layer: 'track', 'dog_park') THEN leisure ELSE NULL END)) AS leisure, ('military_' || (CASE WHEN military IN ('danger_area') THEN military ELSE NULL END)) AS military, ('natural_' || (CASE WHEN "natural" IN ('beach', 'shoal', 'heath', 'grassland', 'wood', 'sand', 'scree', 'shingle', 'bare_rock', 'scrub') THEN "natural" ELSE NULL END)) AS "natural", - ('wetland_' || (CASE WHEN "natural" IN ('wetland', 'marsh', 'mud') THEN (CASE WHEN "natural" IN ('marsh', 'mud') THEN "natural" ELSE wetland END) ELSE NULL END)) AS wetland, + ('wetland_' || (CASE WHEN "natural" IN ('wetland', 'marsh', 'mud') THEN (CASE WHEN "natural" IN ('marsh', 'mud') THEN "natural" ELSE tags->'wetland' END) ELSE NULL END)) AS wetland, ('power_' || (CASE WHEN power IN ('station', 'sub_station', 'substation', 'generator') THEN power ELSE NULL END)) AS power, ('tourism_' || (CASE WHEN tourism IN ('attraction', 'camp_site', 'caravan_site', 'picnic_site') THEN tourism ELSE NULL END)) AS tourism, ('highway_' || (CASE WHEN highway IN ('services', 'rest_area') THEN highway ELSE NULL END)) AS highway, @@ -165,7 +165,7 @@ Layer: OR highway IN ('services', 'rest_area') OR railway = 'station') AND way_area > 0.01*!pixel_width!::real*!pixel_height!::real - ORDER BY CASE WHEN layer~E'^-?\\d+$' AND length(layer)<10 THEN layer::integer ELSE 0 END, way_area DESC + ORDER BY COALESCE(layer,0), way_area DESC ) AS landcover ) AS features properties: @@ -192,7 +192,7 @@ Layer: <<: *osm2pgsql table: |- (SELECT - way, waterway, intermittent, + way, waterway, tags->'intermittent' as intermittent, CASE WHEN tunnel IN ('yes', 'culvert') THEN 'yes' ELSE 'no' END AS int_tunnel FROM planet_osm_line WHERE waterway IN ('stream', 'drain', 'ditch') @@ -209,7 +209,7 @@ Layer: (SELECT way, waterway, - intermittent + tags->'intermittent' as intermittent FROM planet_osm_line WHERE waterway = 'river' ) AS water_lines_low_zoom @@ -246,7 +246,7 @@ Layer: 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 + ORDER BY COALESCE(layer,0), way_area DESC ) AS water_areas properties: minzoom: 4 @@ -262,10 +262,10 @@ Layer: COALESCE(CASE WHEN landuse = 'forest' THEN 'wood' ELSE NULL END, "natural") AS "natural", CASE WHEN "natural" IN ('marsh', 'mud') THEN "natural" - ELSE CASE WHEN ("natural" = 'wetland' AND wetland IS NULL) + ELSE CASE WHEN ("natural" = 'wetland' AND NOT tags ? 'wetland') THEN 'wetland' ELSE CASE WHEN ("natural" = 'wetland') - THEN wetland + THEN tags->'wetland' ELSE NULL END END @@ -274,7 +274,7 @@ Layer: WHERE ("natural" IN ('marsh', 'mud', 'wetland', 'wood', 'beach', 'shoal', 'reef', 'scrub') OR landuse = 'forest') AND building IS NULL AND way_area > 0.01*!pixel_width!::real*!pixel_height!::real - ORDER BY z_order, way_area DESC + ORDER BY COALESCE(layer,0), way_area DESC ) AS landcover_area_symbols properties: minzoom: 10 @@ -296,13 +296,13 @@ Layer: <<: *osm2pgsql table: |- (SELECT - way, waterway, name, intermittent, + way, waterway, name, tags->'intermittent' as intermittent, CASE WHEN tunnel IN ('yes', 'culvert') THEN 'yes' ELSE 'no' END AS int_tunnel, 'no' AS bridge FROM planet_osm_line WHERE waterway IN ('river', 'canal', 'derelict_canal', 'stream', 'drain', 'ditch', 'wadi') AND (bridge IS NULL OR bridge NOT IN ('yes', 'aqueduct')) - ORDER BY z_order + ORDER BY COALESCE(layer,0) ) AS water_lines properties: minzoom: 12 @@ -428,7 +428,7 @@ Layer: AND (amenity IS NULL OR amenity != 'place_of_worship') AND building != 'train_station' AND way_area > 0.01*!pixel_width!::real*!pixel_height!::real - ORDER BY z_order, way_area DESC + ORDER BY COALESCE(layer,0), way_area DESC ) AS buildings properties: minzoom: 13 @@ -449,7 +449,7 @@ Layer: AND building != 'no' AND (aeroway = 'terminal' OR amenity = 'place_of_worship' OR building = 'train_station') AND way_area > 0.01*!pixel_width!::real*!pixel_height!::real - ORDER BY z_order, way_area DESC) + ORDER BY COALESCE(layer,0), way_area DESC) AS buildings_major properties: minzoom: 13 @@ -505,7 +505,7 @@ Layer: WHEN substr(highway, length(highway)-3, 4) = 'link' THEN 'yes' ELSE 'no' END AS link, - CASE WHEN layer~E'^-?\\d+$' AND length(layer)<10 THEN layer::integer ELSE 0 END AS layernotnull + COALESCE(layer,0) AS layernotnull FROM planet_osm_line WHERE (tunnel = 'yes' OR tunnel = 'building_passage' OR covered = 'yes') AND highway IS NOT NULL -- end of road select @@ -531,7 +531,7 @@ Layer: construction, CASE WHEN service IN ('parking_aisle', 'drive-through', 'driveway') THEN 'INT-minor'::text ELSE 'INT-normal'::text END AS service, 'no' AS link, - CASE WHEN layer~E'^-?\\d+$' AND length(layer)<10 THEN layer::integer ELSE 0 END AS layernotnull + COALESCE(layer,0) AS layernotnull FROM planet_osm_line WHERE (tunnel = 'yes' OR tunnel = 'building_passage' OR covered = 'yes') AND (railway IS NOT NULL OR aeroway IS NOT NULL) -- end of rail/aero select @@ -722,7 +722,7 @@ Layer: FROM planet_osm_polygon WHERE highway IN ('residential', 'unclassified', 'pedestrian', 'service', 'footway', 'track', 'path', 'platform') OR railway IN ('platform') - ORDER BY z_order, way_area DESC + ORDER BY COALESCE(layer,0), way_area DESC ) AS highway_area_casing properties: minzoom: 14 @@ -774,7 +774,7 @@ Layer: WHEN substr(highway, length(highway)-3, 4) = 'link' THEN 'yes' ELSE 'no' END AS link, - CASE WHEN layer~E'^-?\\d+$' AND length(layer)<10 THEN layer::integer ELSE 0 END AS layernotnull + COALESCE(layer,0) AS layernotnull FROM planet_osm_line WHERE (tunnel IS NULL OR NOT tunnel IN ('yes', 'building_passage')) AND (covered IS NULL OR NOT covered = 'yes') @@ -802,7 +802,7 @@ Layer: construction, CASE WHEN service IN ('parking_aisle', 'drive-through', 'driveway') THEN 'INT-minor'::text ELSE 'INT-normal'::text END AS service, 'no' AS link, - CASE WHEN layer~E'^-?\\d+$' AND length(layer)<10 THEN layer::integer ELSE 0 END AS layernotnull + COALESCE(layer,0) AS layernotnull FROM planet_osm_line WHERE (tunnel IS NULL OR NOT tunnel IN ('yes', 'building_passage')) AND (covered IS NULL OR NOT covered = 'yes') @@ -882,7 +882,7 @@ Layer: WHERE highway IN ('residential', 'unclassified', 'pedestrian', 'service', 'footway', 'living_street', 'track', 'path', 'platform', 'services') OR railway IN ('platform') OR aeroway IN ('runway', 'taxiway', 'helipad') - ORDER BY z_order, way_area desc + ORDER BY COALESCE(layer,0), way_area desc ) AS highway_area_fill properties: minzoom: 14 @@ -939,7 +939,7 @@ Layer: WHEN substr(highway, length(highway)-3, 4) = 'link' THEN 'yes' ELSE 'no' END AS link, - CASE WHEN layer~E'^-?\\d+$' AND length(layer)<10 THEN layer::integer ELSE 0 END AS layernotnull + COALESCE(layer,0) AS layernotnull FROM planet_osm_line WHERE (tunnel IS NULL OR NOT tunnel IN ('yes', 'building_passage')) AND (covered IS NULL OR NOT covered = 'yes') @@ -967,7 +967,7 @@ Layer: construction, CASE WHEN service IN ('parking_aisle', 'drive-through', 'driveway') THEN 'INT-minor'::text ELSE 'INT-normal'::text END AS service, 'no' AS link, - CASE WHEN layer~E'^-?\\d+$' AND length(layer)<10 THEN layer::integer ELSE 0 END AS layernotnull + COALESCE(layer,0) AS layernotnull FROM planet_osm_line WHERE (tunnel IS NULL OR NOT tunnel IN ('yes', 'building_passage')) AND (covered IS NULL OR NOT covered = 'yes') @@ -1098,7 +1098,7 @@ Layer: WHERE highway IS NOT NULL OR (railway IS NOT NULL AND railway != 'preserved' AND (service IS NULL OR service NOT IN ('spur', 'siding', 'yard'))) - ORDER BY z_order + ORDER BY COALESCE(layer,0) ) AS roads_low_zoom properties: minzoom: 5 @@ -1115,13 +1115,13 @@ Layer: way, waterway, name, - intermittent, + tags->'intermittent' as intermittent, CASE WHEN tunnel IN ('yes', 'culvert') THEN 'yes' ELSE 'no' END AS int_tunnel, 'yes' AS bridge FROM planet_osm_line WHERE waterway IN ('river', 'canal', 'derelict_canal', 'stream', 'drain', 'ditch', 'wadi') AND bridge IN ('yes', 'aqueduct') - ORDER BY z_order + ORDER BY COALESCE(layer,0) ) AS waterway_bridges properties: minzoom: 12 @@ -1173,7 +1173,7 @@ Layer: WHEN substr(highway, length(highway)-3, 4) = 'link' THEN 'yes' ELSE 'no' END AS link, - CASE WHEN layer~E'^-?\\d+$' AND length(layer)<10 THEN layer::integer ELSE 0 END AS layernotnull + COALESCE(layer,0) AS layernotnull FROM planet_osm_line WHERE bridge IN ('yes', 'boardwalk', 'cantilever', 'covered', 'low_water_crossing', 'movable', 'trestle', 'viaduct') AND highway IS NOT NULL -- end of road select @@ -1199,7 +1199,7 @@ Layer: construction, CASE WHEN service IN ('parking_aisle', 'drive-through', 'driveway') THEN 'INT-minor'::text ELSE 'INT-normal'::text END AS service, 'no' AS link, - CASE WHEN layer~E'^-?\\d+$' AND length(layer)<10 THEN layer::integer ELSE 0 END AS layernotnull + COALESCE(layer,0) AS layernotnull FROM planet_osm_line WHERE bridge IN ('yes', 'boardwalk', 'cantilever', 'covered', 'low_water_crossing', 'movable', 'trestle', 'viaduct') AND (railway IS NOT NULL OR aeroway IS NOT NULL) -- end of rail/aero select @@ -1445,13 +1445,13 @@ Layer: way, name, CASE - WHEN (population ~ '^[0-9]{1,8}$') THEN population::INTEGER ELSE 0 + WHEN (tags->'population' ~ '^[0-9]{1,8}$') THEN (tags->'population')::INTEGER ELSE 0 END as population, round(ascii(md5(osm_id::text)) / 55) AS dir -- base direction factor on geometry to be consistent across metatiles FROM planet_osm_point WHERE place IN ('city', 'town', 'village', 'hamlet') AND name IS NOT NULL - AND capital = 'yes' + AND tags @> 'capital=>yes' ORDER BY population DESC ) AS capital_names properties: @@ -1503,21 +1503,21 @@ Layer: name, ( (CASE - WHEN (population ~ '^[0-9]{1,8}$') THEN population::INTEGER + WHEN (tags->'population' ~ '^[0-9]{1,8}$') THEN (tags->'population')::INTEGER WHEN (place = 'city') THEN 100000 WHEN (place = 'town') THEN 1000 ELSE 1 END) * (CASE - WHEN (capital = '4') THEN 2 + WHEN (tags @> 'capital=>4') THEN 2 ELSE 1 END) ) AS score FROM planet_osm_point WHERE place IN ('city', 'town') AND name IS NOT NULL - AND (capital IS NULL OR capital != 'yes') + AND NOT (tags @> 'capital=>yes') ) as p ORDER BY score DESC, length(name) DESC, name ) AS placenames_medium @@ -1538,7 +1538,7 @@ Layer: FROM planet_osm_point WHERE place IN ('village', 'hamlet') AND name IS NOT NULL - AND (capital IS NULL OR capital != 'yes') + AND NOT tags @> 'capital=>yes' OR place IN ('suburb', 'neighbourhood', 'locality', 'isolated_dwelling', 'farm') AND name IS NOT NULL ORDER BY CASE @@ -1641,9 +1641,9 @@ Layer: ) AS feature, access, religion, - denomination, - "generator:source", - power_source, + tags->'denomination' as denomination, + tags->'generator:source' as "generator:source", + tags->'power_source' as power_source, way_area/NULLIF(!pixel_width!::real*!pixel_height!::real,0) AS way_pixels FROM planet_osm_polygon -- The upcoming where clause is needed for performance only, as the CASE statements would end up doing the equivalent filtering @@ -1664,7 +1664,7 @@ Layer: OR "natural" IN ('spring') OR historic IN ('memorial', 'monument', 'archaeological_site') OR highway IN ('bus_stop', 'elevator', 'traffic_signals') - OR (power = 'generator' AND ("generator:source" = 'wind' OR power_source = 'wind')) + OR (power = 'generator' AND (tags @> '"generator:source"=>wind' OR tags @> 'power_source=>wind')) ORDER BY way_area desc ) AS amenity_points_poly properties: @@ -1715,15 +1715,15 @@ Layer: CASE WHEN "natural" IN ('peak', 'volcano', 'saddle') THEN CASE - WHEN ele ~ '^-?\d{1,4}(\.\d+)?$' THEN ele::NUMERIC + WHEN tags->'ele' ~ '^-?\d{1,4}(\.\d+)?$' THEN (tags->'ele')::NUMERIC ELSE NULL END ELSE NULL END AS score, religion, - denomination, - "generator:source", - power_source, + tags->'denomination' as denomination, + tags->'generator:source' as "generator:source", + tags->'power_source' as power_source, NULL AS way_pixels FROM planet_osm_point -- The upcoming where clause is needed for performance only, as the CASE statements would end up doing the equivalent filtering @@ -1745,7 +1745,7 @@ Layer: OR "natural" IN ('peak', 'volcano', 'saddle', 'spring', 'cave_entrance') OR historic IN ('memorial', 'monument', 'archaeological_site', 'wayside_cross') OR highway IN ('bus_stop', 'elevator', 'traffic_signals', 'ford') - OR (power = 'generator' AND ("generator:source" = 'wind' OR power_source = 'wind')) + OR (power = 'generator' AND (tags @> '"generator:source"=>wind' OR tags @> 'power_source=>wind')) ORDER BY score DESC NULLS LAST ) AS amenity_points properties: @@ -1990,7 +1990,7 @@ Layer: OR junction IN ('roundabout')) ORDER BY prio DESC, -- put important roads first - CASE WHEN layer~E'^-?\\d+$' AND length(layer)<10 THEN layer::integer ELSE 0 END DESC, -- put top layered roads first + COALESCE(layer, 0), -- put top layered roads first length(name) DESC, -- Try to fit big labels in first name DESC, -- Force a consistent ordering between differently named streets l.osm_id DESC -- Force an ordering for streets of the same name, e.g. dualized roads @@ -2102,7 +2102,7 @@ Layer: ) AS feature, access, name, - operator, + tags->'operator' as operator, ref, way_area, CASE WHEN building = 'no' OR building IS NULL THEN 'no' ELSE 'yes' END AS is_building @@ -2144,7 +2144,7 @@ Layer: COALESCE('man_made_' || man_made, 'waterway_' || waterway, 'natural_' || "natural") AS feature, access, name, - operator, + tags->'operator' as operator, ref, NULL AS way_area, CASE WHEN building = 'no' OR building IS NULL THEN 'no' ELSE 'yes' END AS is_building @@ -2235,13 +2235,13 @@ Layer: CASE WHEN "natural" IN ('peak', 'volcano', 'saddle') OR tourism = 'alpine_hut' OR amenity = 'shelter' THEN CASE - WHEN ele ~ '^-?\d{1,4}(\.\d+)?$' THEN ele::NUMERIC + WHEN tags->'ele' ~ '^-?\d{1,4}(\.\d+)?$' THEN (tags->'ele')::NUMERIC ELSE NULL END ELSE NULL END AS elevation, "natural", - operator, + tags->'operator' as operator, ref, NULL AS way_area, CASE WHEN building = 'no' OR building IS NULL THEN 'no' ELSE 'yes' END AS is_building @@ -2266,7 +2266,7 @@ Layer: OR boundary IN ('national_park') OR waterway IN ('dam', 'weir')) AND (name IS NOT NULL - OR (ele IS NOT NULL AND ("natural" IN ('peak', 'volcano', 'saddle') OR tourism = 'alpine_hut' OR amenity = 'shelter')) + OR (tags?'ele' AND ("natural" IN ('peak', 'volcano', 'saddle') OR tourism = 'alpine_hut' OR amenity = 'shelter')) OR (ref IS NOT NULL AND aeroway IN ('gate')) ) ) AS p @@ -2346,13 +2346,13 @@ Layer: waterway, lock, name, - intermittent, + tags->'intermittent' as intermittent, CASE WHEN tunnel IN ('yes', 'culvert') THEN 'yes' ELSE 'no' END AS int_tunnel FROM planet_osm_line WHERE waterway IN ('river', 'canal', 'derelict_canal', 'stream', 'drain', 'ditch', 'wadi') AND (tunnel IS NULL or tunnel != 'culvert') AND name IS NOT NULL - ORDER BY z_order + ORDER BY COALESCE(layer,0) ) AS water_lines_text properties: minzoom: 13 diff --git a/scripts/lua/README.md b/scripts/lua/README.md new file mode 100644 index 0000000000..b5c3a4ef4e --- /dev/null +++ b/scripts/lua/README.md @@ -0,0 +1,5 @@ +# Lua helper scripts # + +These scripts are for developing, testing, and profiling the [Lua tag transform](../../openstreetmap-carto.lua). There is a symlink to the transform in this directory so it can be `require`d by other files. + +They are not necessary for map rendering or most development. diff --git a/scripts/lua/openstreetmap-carto.lua b/scripts/lua/openstreetmap-carto.lua new file mode 120000 index 0000000000..ba19fbe902 --- /dev/null +++ b/scripts/lua/openstreetmap-carto.lua @@ -0,0 +1 @@ +../../openstreetmap-carto.lua \ No newline at end of file diff --git a/scripts/lua/test.lua b/scripts/lua/test.lua new file mode 100644 index 0000000000..ed4d5bbc89 --- /dev/null +++ b/scripts/lua/test.lua @@ -0,0 +1,165 @@ +--[[ +This file is part of OpenStreetMap Carto and used for validating the Lua tag transforms. + +Run it with lua test.lua +]] + +require ("openstreetmap-carto") + +--- compare two tables. +-- @param t1 A table +-- @param t2 A table +-- @return true or false +function equaltables (t1,t2) + for k, v in pairs(t1) do + if t2[k] ~= v then return false end + end + for k, v in pairs(t2) do + if t1[k] ~= v then return false end + end + return true +end + +print("TESTING: z_order") + +assert(z_order({}) == nil, "test failed: no tags") +assert(z_order({foo="bar"}) == nil, "test failed: other tags") +assert(z_order({highway="motorway"}) == 380 , "test failed: motorway") +assert(z_order({highway="motorway", railway="rail"}) == 440 , "test failed: motorway + rail") + +print("TESTING: roads") +assert(roads({}) == 0, "test failed: no tags") +assert(roads({foo="bar"}) == 0, "test failed: other tags") +assert(roads({highway="motorway"}) == 1, "test failed: motorway") +assert(roads({railway="rail"}) == 1, "test failed: rail") +assert(roads({highway="residential", railway="rail"}) == 1, "test failed: rail+residential") +assert(roads({railway="turntable"}) == 0, "test failed: rail=turntable") +assert(roads({railway="rail", service="spur"}) == 0, "test failed: rail SSY") +assert(roads({railway="rail", service="main"}) == 1, "test failed: rail non-SSY") +assert(roads({boundary="administrative"}) == 1, "test failed: boundary administrative") + +print("TESTING: isarea") +assert(isarea({}) == 0, "test failed: no tags") +assert(isarea({foo = "bar"}) == 0, "test failed: random tag") +assert(isarea({area = "yes"}) == 1, "test failed: explicit area") +assert(isarea({area = "no"}) == 0, "test failed: explicit not area") +assert(isarea({area = "no", landuse = "forest"}) == 0, "test failed: explicit not area with polygon tag") +assert(isarea({leisure = "track"}) == 0, "test failed: leisure=track") +assert(isarea({area = "yes", leisure = "track"}) == 1, "test failed: leisure=track with area tag") +assert(isarea({waterway = "river"}) == 0, "test failed: river") +assert(isarea({waterway = "riverbank"}) == 1, "test failed: river") +assert(isarea({highway = "services"}) == 1, "test failed: river") + +print("TESTING: filter_tags_generic") +assert(({filter_tags_generic({})})[1] == 1, "Untagged filter") +assert(equaltables(({filter_tags_generic({})})[2], {}), "Untagged tags") +assert(({filter_tags_generic({note="foo"})})[1] == 1, "deleted filter") +assert(equaltables(({filter_tags_generic({note="foo"})})[2], {}), "deleted tags") +assert(({filter_tags_generic({foo="bar"})})[1] == 0, "single tag filter") +assert(equaltables(({filter_tags_generic({foo="bar"})})[2], {foo="bar"}), "single tag tags") +assert(({filter_tags_generic({foo="bar", note="baz"})})[1] == 0, "tag + deleted tag filter") +assert(equaltables(({filter_tags_generic({foo="bar", note="baz"})})[2], {foo="bar"}), "tag + deleted tags") +assert(({filter_tags_generic({["note:xx"]="foo"})})[1] == 1, "wildcard deleted filter") +assert(equaltables(({filter_tags_generic({["note:xx"]="foo"})})[2], {}), "wildcard deleted tags") +assert(({filter_tags_generic({["note:xx"]="foo", foo="bar"})})[1] == 0, "wildcard deleted + tag filter") +assert(equaltables(({filter_tags_generic({["note:xx"]="foo", foo="bar"})})[2], {foo="bar"}), "wildcard deleted + tag tags") + +assert(({filter_tags_generic({["foo:note:xx"]="foo"})})[1] == 0, "prefix later in tag filter") +assert(equaltables(({filter_tags_generic({["foo:note:xx"]="foo"})})[2], {["foo:note:xx"]="foo"}), "prefix later in tag tags") + +print("TESTING: filter_tags_relation_member") + +--- Tests filter_tags_relation_member against expected values +-- @param keyvalues OSM tags, after processing by relation transform +-- @param keyvaluemembers OSM tags of relation members, after processing by way transform +-- @param filter expected filter result +-- @param cols expected cols result +-- @param member_superseded expected member_superseded result +-- @param boundary expected boundary result +-- @param polygon expected polygon result +-- @param roads expected roads result +local function check_rel_member(keyvalues, keyvaluemembers, filter, cols, member_superseded, boundary, polygon, roads) + + local i = 0 + for _ in pairs(keyvaluemembers) do + i = i + 1 + end + + local actual_filter, actual_cols, actual_member_superseded, actual_boundary, actual_polygon, actual_roads + = filter_tags_relation_member(keyvalues, keyvaluemembers, nil, i) + + if actual_filter ~= filter then + print("filter mismatch") + return false + end + if not equaltables(actual_cols, cols) then + print("cols mismatch") + return false + end + if not equaltables(actual_member_superseded, member_superseded) then + print("member_superseded mismatch, actual table was") + for i, v in ipairs(actual_member_superseded) do + print(i, v) + end + return false + end + if actual_boundary ~= boundary then + print("boundary mismatch") + return false + end + if actual_polygon ~= polygon then + print("polygon mismatch") + return false + end + if actual_roads ~= roads then + print("roads mismatch") + return false + end + return true +end + +assert(check_rel_member({}, {}, 1, {}, {}, 0, 0, 0), "test failed: untagged memberless relation") +assert(check_rel_member({}, {{}}, 1, {}, {0}, 0, 0, 0), "test failed: untagged relation") + +assert(check_rel_member({type="multipolygon"}, {{}}, 1, {}, {0}, 0, 0, 0), + "test failed: untagged MP") +assert(check_rel_member({type="multipolygon", foo="bar"}, {{}}, 0, {foo="bar"}, {0}, 0, 1, 0), + "test failed: MP with tag") + +-- New-style MPs +assert(check_rel_member({type="multipolygon", foo="bar"}, {{},{}}, 0, {foo="bar"}, {0,0}, 0, 1, 0), + "test failed: MP with tag, two ways") +assert(check_rel_member({type="multipolygon", foo="bar"}, {{baz="qax"}}, 0, {foo="bar"}, {0}, 0, 1, 0), + "test failed: MP with tag, way with different tag") +assert(check_rel_member({type="multipolygon", foo="bar"}, {{baz="qax"}, {}}, 0, {foo="bar"}, {0,0}, 0, 1, 0), + "test failed: MP with tag, way with different tag + untagged way") +assert(check_rel_member({type="multipolygon", foo="bar"}, {{foo="bar"}}, 0, {foo="bar"}, {0}, 0, 1, 0), + "test failed: MP with tag, way with same tag") +assert(check_rel_member({type="multipolygon", foo="bar"}, {{foo="bar"},{}}, 0, {foo="bar"}, {0,0}, 0, 1, 0), + "test failed: MP with tag, way with same tag + untagged way") +assert(check_rel_member({type="multipolygon", foo="bar"}, {{foo="bar"}, {baz="qax"}}, 0, {foo="bar"}, {0,0}, 0, 1, 0), + "test failed: MP with tag, way with same tag") + +-- Old-style MPs +assert(check_rel_member({type="multipolygon"}, {{foo="bar"}}, 1, {}, {0}, 0, 0, 0), + "test failed: MP w/o tag, way with tag") +assert(check_rel_member({type="multipolygon"}, {{foo="bar"}, {}}, 1, {}, {0,0}, 0, 0, 0), + "test failed: MP w/o tag, way with tag + untagged way") +assert(check_rel_member({type="multipolygon"}, {{foo="bar"}, {baz="qax"}}, 1, {}, {0,0}, 0, 0, 0), + "test failed: MP w/o tag, way with tag + way with other tag") + +-- Boundary relations +assert(check_rel_member({type="boundary"}, {{}}, 1, {}, {0}, 0, 0, 0), + "test failed: untagged boundary") +assert(check_rel_member({type="boundary", boundary="administrative"}, {{}}, 0, {boundary="administrative"}, {0}, 1, 0, 1), + "test failed: untagged boundary") +assert(check_rel_member({type="boundary", boundary="administrative"}, {{}}, 0, {boundary="administrative"}, {0}, 1, 0, 1), + "test failed: untagged boundary") +assert(check_rel_member({type="boundary", boundary="administrative"}, {{foo="bar"}}, 0, {boundary="administrative"}, {0}, 1, 0, 1), + "test failed: untagged boundary, tagged way") + +-- Route relations +assert(check_rel_member({type="route"}, {{}}, 1, {}, {0}, 0, 0, 0), + "test failed: untagged route") +assert(check_rel_member({type="route", route="road"}, {{}}, 0, {route="road"}, {0}, 1, 0, 0), + "test failed: tagged route")