Skip to content

Commit 59cb0ad

Browse files
committed
define a schema for relation members
1 parent 7c14759 commit 59cb0ad

File tree

4 files changed

+167
-7
lines changed

4 files changed

+167
-7
lines changed

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,67 @@ For example,
386386
}
387387
```
388388

389+
##### `relation`
390+
391+
For relations, this object describes which roles are allowed, which tags are required for each role, and other constraints related to relation roles.
392+
393+
* `relation.id` – a string. This is the “permanent relation type ID”, it must match the value of [permanent relation type ID<sup><code>P41</code></sup>](https://osm.wiki/Property:P41) in the OSM wiki’s wikibase system.
394+
* `relation.allowDuplicateMembers` – a boolean. Set to `true` if the same OSM feature is allowed to appear multiple times in the relation's members.
395+
* `relation.members` – an array of objects, which lists every relation role that is permitted as a member (see below).
396+
397+
A full example looks like this:
398+
399+
```jsonc
400+
// type/restriction/no_u_turn.json
401+
{
402+
"relation": {
403+
"id": "restriction", // the value of https://osm.wiki/Property:P41 from the matching
404+
// item, in this case https://wiki.openstreetmap.org/wiki/Item:Q16054. Note that multiple
405+
// relation entries may have the same `id` value.
406+
"allowDuplicateMembers": true,
407+
"members": [
408+
{
409+
"role": "from", // The relation role. An empty string is allowed.
410+
"roleLabel": "From", // The label for the role, in the default language. An empty string is allowed.
411+
"geometry": ["line"], // If not specified, any geometry is allowed.
412+
"matchTags": [
413+
// Describes which tags the member must have, if it has this role.
414+
// `*` can be used as a tag value.
415+
// If multiple array items are specified, only 1 needs to match.
416+
// If this property is not specified, then any tags are allowed
417+
{ "highway": "*" }
418+
],
419+
"min": 1, // The minimum number of times that this role must appear in the relation.
420+
"max": 1 // The maximum number of times that this role must appear in the relation.
421+
},
422+
{
423+
"role": "via",
424+
"roleLabel": "Via",
425+
"geometry": ["vertex", "line"],
426+
"min": 1
427+
},
428+
{
429+
"role": "to",
430+
"roleLabel": "To",
431+
"geometry": ["line"],
432+
"matchTags": [{ "highway": "*" }],
433+
"min": 1,
434+
"max": 1
435+
}
436+
]
437+
}
438+
}
439+
```
440+
441+
##### `relationCrossReference`
442+
443+
To avoid repeating the [`relation` object](#relation) in several presets, you can use `relationCrossReference` to reference another preset.
444+
445+
For example:
446+
```js
447+
"relationCrossReference": "{type/route}"
448+
```
449+
389450
### Fields
390451

391452
Fields are reusable form elements that can be associated with presets.

lib/build.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { fileURLToPath } from 'node:url';
22
import { styleText } from 'node:util';
33
import fs from 'fs';
4-
import jsonschema from 'jsonschema';
4+
import { Validator } from 'jsonschema';
55
import path from 'path';
66
import shell from 'shelljs';
77
import YAML from 'js-yaml';
@@ -22,6 +22,8 @@ const discardedSchema = require('../schemas/discarded.json');
2222

2323
let _currBuild = null;
2424

25+
const jsonschema = new Validator();
26+
2527
function validateData(options) {
2628
const START = '🔬 ' + styleText('yellow', 'Validating schema...');
2729
const END = '👍 ' + styleText('green', 'schema okay');
@@ -205,13 +207,16 @@ function read(f) {
205207

206208

207209
function validateSchema(file, instance, schema) {
210+
// add this schema to the cache, so $ref can be resolved faster
211+
jsonschema.addSchema(schema);
212+
208213
let validationErrors = jsonschema.validate(instance, schema).errors;
209214

210215
if (validationErrors.length) {
211216
process.stderr.write(`${file}: \n`);
212217
validationErrors.forEach(error => {
213218
if (error.property) {
214-
process.stderr.write(error.property + ' ' + error.message);
219+
process.stderr.write(error.property + ' ' + error.message + '\n');
215220
} else {
216221
process.stderr.write(error + '\n');
217222
}
@@ -366,6 +371,13 @@ function generatePresets(dataDir, tstrings, searchableFieldIDs, listReusedIcons)
366371
if (!icons[icon]) icons[icon] = [];
367372
icons[icon].push(id);
368373
}
374+
375+
if (preset.relation) {
376+
tstrings.presets[id].relation_roles = {};
377+
for (const member of preset.relation.members) {
378+
tstrings.presets[id].relation_roles[member.role] = member.roleLabel;
379+
}
380+
}
369381
});
370382

371383
if (listReusedIcons) {
@@ -465,8 +477,10 @@ function generateTranslations(fields, presets, tstrings, searchableFieldIDs) {
465477
let tags = preset.tags || {};
466478
let keys = Object.keys(tags);
467479

480+
const tagsString = keys.map(k => `${k}=${tags[k]}`).join(' + ');
481+
468482
if (keys.length) {
469-
yamlPreset['#name'] = keys.map(k => `${k}=${tags[k]}`).join(' + ');
483+
yamlPreset['#name'] = tagsString;
470484
if (yamlPreset.aliases) {
471485
yamlPreset['#name'] += ' | ' + yamlPreset.aliases.split('\n').join(', ');
472486
}
@@ -477,6 +491,12 @@ function generateTranslations(fields, presets, tstrings, searchableFieldIDs) {
477491
yamlPreset['#name'] += ` | Local preset for countries ${preset.locationSet.include.map(country => `"${country.toUpperCase()}"`).join(', ')}`;
478492
}
479493

494+
if (yamlPreset.relation_roles) {
495+
for (const role in yamlPreset.relation_roles) {
496+
yamlPreset.relation_roles[`#${role}`] = `Relation role “${role}” when used with ${tagsString}`;
497+
}
498+
}
499+
480500
if (preset.searchable !== false) {
481501
if (yamlPreset.terms) {
482502
yamlPreset['#terms'] = 'terms: ' + yamlPreset.terms;

schemas/field.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@
9797
"minItems": 1,
9898
"uniqueItems": true,
9999
"items": {
100-
"type": "string",
101-
"enum": ["point", "vertex", "line", "area", "relation"]
100+
"$ref": "#/$defs/Geometry"
102101
}
103102
},
104103
"default": {
@@ -382,5 +381,11 @@
382381
{ "required": ["keys"] }
383382
]}
384383
]}
385-
]
384+
],
385+
"$defs": {
386+
"Geometry": {
387+
"type": "string",
388+
"enum": ["point", "vertex", "line", "area", "relation"]
389+
}
390+
}
386391
}

schemas/preset.json

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,82 @@
135135
},
136136
"minProperties": 1,
137137
"additionalProperties": false
138+
},
139+
"relation": {
140+
"$ref": "#/$defs/RelationSchema"
141+
},
142+
"relationCrossReference": {
143+
"description": "A preset can reference the relation schema from another preset, instead of defining the same schema again. If present, then the `relation` property must not appear.",
144+
"type": "string",
145+
"pattern": "^\\{.+\\}$"
138146
}
139147
},
140148
"additionalProperties": false,
141-
"required": ["name", "geometry", "tags"]
149+
"required": ["name", "geometry", "tags"],
150+
"$defs": {
151+
"RelationSchema": {
152+
"type": "object",
153+
"properties": {
154+
"id": {
155+
"type": "string",
156+
"description": "The “permanent relation type ID”, this should match the value of https://osm.wiki/Property:P41 in the OSM wiki’s wikibase system."
157+
},
158+
"allowDuplicateMembers": {
159+
"type": "boolean",
160+
"description": "Set to `true` if the relation can contain the same member multiple times."
161+
},
162+
"members": {
163+
"type": "array",
164+
"items": {
165+
"type": "object",
166+
"properties": {
167+
"role": {
168+
"type": "string",
169+
"description": "The relation role. An empty string is allowed."
170+
},
171+
"roleLabel": {
172+
"type": "string",
173+
"description": "The label for the role, in the default language. An empty string is allowed."
174+
},
175+
"geometry": {
176+
"type": "array",
177+
"uniqueItems": true,
178+
"items": {
179+
"$ref": "field.json#/$defs/Geometry"
180+
},
181+
"description": "If not specified, any geometry is allowed"
182+
},
183+
"matchTags": {
184+
"type": "array",
185+
"items": {
186+
"type": "object",
187+
"additionalProperties": {
188+
"type": "string"
189+
},
190+
"minProperties": 1
191+
},
192+
"examples": [
193+
[{ "a": 1, "b": 2 }],
194+
[{ "a": 1 }, { "b": 2 }]
195+
],
196+
"description": "`*` can be used as a tag value. If multiple array items are specified, only 1 needs to match. If not specified, then any tags are allowed"
197+
},
198+
"min": {
199+
"type": "integer",
200+
"description": "If unspecified, there is no minimum for how many times this role can appear in the relation"
201+
},
202+
"max": {
203+
"type": "integer",
204+
"description": "If unspecified, there is no maximum for how many times this role can appear in the relation"
205+
}
206+
},
207+
"required": ["role", "roleLabel"],
208+
"additionalProperties": false
209+
}
210+
}
211+
},
212+
"required": ["id", "allowDuplicateMembers", "members"],
213+
"additionalProperties": false
214+
}
215+
}
142216
}

0 commit comments

Comments
 (0)