diff --git a/lib/build.js b/lib/build.js index 7bea5dc..625f28c 100644 --- a/lib/build.js +++ b/lib/build.js @@ -1,7 +1,7 @@ import chalk from 'chalk'; import fs from 'fs'; import { globSync } from 'glob'; -import jsonschema from 'jsonschema'; +import { Validator } from 'jsonschema'; import path from 'path'; import shell from 'shelljs'; import YAML from 'js-yaml'; @@ -21,6 +21,8 @@ const discardedSchema = require('../schemas/discarded.json'); let _currBuild = null; +const jsonschema = new Validator(); + function validateData(options) { const START = '🔬 ' + chalk.yellow('Validating schema...'); const END = '👍 ' + chalk.green('schema okay'); @@ -203,6 +205,9 @@ function read(f) { function validateSchema(file, instance, schema) { + // add this schema to the cache, so $ref can be resolved faster + jsonschema.addSchema(schema); + let validationErrors = jsonschema.validate(instance, schema).errors; if (validationErrors.length) { @@ -362,6 +367,15 @@ function generatePresets(dataDir, tstrings, searchableFieldIDs, listReusedIcons) if (!icons[icon]) icons[icon] = []; icons[icon].push(id); } + + if (preset.relation) { + tstrings.presets[id].relation_roles = {}; + for (const member of preset.relation.members) { + for (const role in member.roles) { + tstrings.presets[id].relation_roles[role] = member.roles[role]; + } + } + } }); if (listReusedIcons) { @@ -454,8 +468,10 @@ function generateTranslations(fields, presets, tstrings, searchableFieldIDs) { let tags = preset.tags || {}; let keys = Object.keys(tags); + const tagsString = keys.map(k => `${k}=${tags[k]}`).join(' + '); + if (keys.length) { - yamlPreset['#name'] = keys.map(k => `${k}=${tags[k]}`).join(' + '); + yamlPreset['#name'] = tagsString; if (yamlPreset.aliases) { yamlPreset['#name'] += ' | ' + yamlPreset.aliases.split('\n').join(', '); } @@ -466,6 +482,12 @@ function generateTranslations(fields, presets, tstrings, searchableFieldIDs) { yamlPreset['#name'] += ` | Local preset for countries ${preset.locationSet.include.map(country => `"${country.toUpperCase()}"`).join(', ')}`; } + if (yamlPreset.relation_roles) { + for (const role in yamlPreset.relation_roles) { + yamlPreset.relation_roles[`#${role}`] = `Relation role “${role}” when used with ${tagsString}`; + } + } + if (preset.searchable !== false) { if (yamlPreset.terms) { yamlPreset['#terms'] = 'terms: ' + yamlPreset.terms; diff --git a/schemas/field.json b/schemas/field.json index 66b4af6..be4dd15 100644 --- a/schemas/field.json +++ b/schemas/field.json @@ -92,8 +92,7 @@ "minItems": 1, "uniqueItems": true, "items": { - "type": "string", - "enum": ["point", "vertex", "line", "area", "relation"] + "$ref": "#/$defs/Geometry" } }, "default": { @@ -294,5 +293,11 @@ { "required": ["keys"] } ]} ]} - ] + ], + "$defs": { + "Geometry": { + "type": "string", + "enum": ["point", "vertex", "line", "area", "relation"] + } + } } diff --git a/schemas/preset.json b/schemas/preset.json index c773a03..68f31d2 100644 --- a/schemas/preset.json +++ b/schemas/preset.json @@ -126,8 +126,88 @@ } }, "additionalProperties": false + }, + "relation": { + "$ref": "#/$defs/RelationSchema" + }, + "relationCrossReference": { + "description": "A preset can reference the relation schema from another preset", + "type": "string", + "pattern": "^\\{.+\\}$" } }, "additionalProperties": false, - "required": ["name", "geometry", "tags"] + "required": ["name", "geometry", "tags"], + "$defs": { + "RelationSchema": { + "type": "object", + "properties": { + "optionalTags": { + "type": "object", + "description": "Only useful for specifying placeholders which are referenced in members.*.matchTags", + "examples": [{ "route": "$1" }], + "additionalProperties": { + "type": "string" + } + }, + "id": { + "type": "string", + "description": "The “permanent relation type ID”, this should match the value of https://osm.wiki/Property:P41 in the OSM wiki’s wikibase system." + }, + "allowDuplicateMembers": { + "type": "boolean", + "default": true + }, + "members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "roles": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Map of roles to their label in the default language. An empty string is allowed as key." + }, + "geometry": { + "type": "array", + "items": { + "$ref": "field.json#/$defs/Geometry" + }, + "description": "If not specified, any geometry is allowed" + }, + "matchTags": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "examples": [ + [{}], + [{ "a": 1, "b": 2 }], + [{ "a": 1 }, { "b": 2 }] + ], + "description": "`*` can be used as a tag value. If multiple array items are specified, only 1 needs to match." + }, + "min": { + "type": "integer", + "description": "If unspecified, there is no minimum" + }, + "max": { + "type": "integer", + "description": "If unspecified, there is no maximum" + } + }, + "required": ["matchTags"], + "additionalProperties": false + } + } + }, + "required": ["id", "allowDuplicateMembers", "members"], + "additionalProperties": false + } + } }