Skip to content

Commit a914da9

Browse files
committed
define a schema for relation members
1 parent f15c5a2 commit a914da9

File tree

4 files changed

+203
-6
lines changed

4 files changed

+203
-6
lines changed

README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,96 @@ 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 should 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.optionalTags` – an object, only useful for specifying placeholders which are referenced in `members.*.matchTags`. See example below.
396+
* `relation.members` – an array of objects, which lists every member that is permitted (see below).
397+
398+
A full example looks like this:
399+
400+
```jsonc
401+
// type/restriction/no_left_turn.json
402+
{
403+
"relation": {
404+
"id": "restriction", // value of https://osm.wiki/Property:P41
405+
"allowDuplicateMembers": true,
406+
"members": [
407+
{
408+
"role": "from", // The relation role. An empty string is allowed
409+
"roleLabel": "From", // The label for the role, in the default language.
410+
"geometry": ["line"], // If not specified, any geometry is allowed
411+
"matchTags": [
412+
// Describes which tags the member must have, if it has this role.
413+
// `*` can be used as a tag value.
414+
// If multiple array items are specified, only 1 needs to match.
415+
// If this property is not specified, then any tags are allowed
416+
{ "highway": "*" }
417+
],
418+
"min": 1, // minimum number of times that this role must appear in the relation
419+
"max": 1, // maxmium number of times that this role must appear in the relation
420+
},
421+
{
422+
"role": "via",
423+
"roleLabel": "Via",
424+
"geometry": ["vertex", "line"],
425+
"min": 1,
426+
"max": 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+
It is possible for the `matchTags` constraints to reference tags from the relation.
442+
443+
For example, if a relation with [`type=route`](https://osm.wiki/Tag:type=route) + [`route=XXXX`](https://osm.wiki/Key:route) has a member with a role of `stop`, then that member must have a tag of `XXXX=yes`<sup>[[1]](https://osm.wiki/Relation:route#Members)</sup>. This can be expressed using the following syntax:
444+
445+
```jsonc
446+
// type/route.json
447+
{
448+
"relation": {
449+
"optionalTags": {
450+
"route": "$1" // 👈 here
451+
},
452+
"members": [
453+
{
454+
"role": "stop",
455+
"roleLabel": "Stop Position",
456+
"geometry": ["point", "vertex"],
457+
"matchTags": [
458+
{
459+
"public_transport": "stop_position",
460+
"$1": "yes" // 👈 here
461+
},
462+
],
463+
},
464+
],
465+
},
466+
}
467+
```
468+
469+
470+
##### `relationCrossReference`
471+
472+
To avoid repeating the [`relation` object](#relation) in several presets, you can use `relationCrossReference` to reference another preset.
473+
474+
For example:
475+
```js
476+
"relationCrossReference": "{type/route}"
477+
```
478+
389479
### Fields
390480

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

lib/build.js

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

2121
let _currBuild = null;
2222

23+
const jsonschema = new Validator();
24+
2325
function validateData(options) {
2426
const START = '🔬 ' + styleText('yellow', 'Validating schema...');
2527
const END = '👍 ' + styleText('green', 'schema okay');
@@ -202,6 +204,9 @@ function read(f) {
202204

203205

204206
function validateSchema(file, instance, schema) {
207+
// add this schema to the cache, so $ref can be resolved faster
208+
jsonschema.addSchema(schema);
209+
205210
let validationErrors = jsonschema.validate(instance, schema).errors;
206211

207212
if (validationErrors.length) {
@@ -363,6 +368,15 @@ function generatePresets(dataDir, tstrings, searchableFieldIDs, listReusedIcons)
363368
if (!icons[icon]) icons[icon] = [];
364369
icons[icon].push(id);
365370
}
371+
372+
if (preset.relation) {
373+
tstrings.presets[id].relation_roles = {};
374+
for (const member of preset.relation.members) {
375+
for (const role in member.roles) {
376+
tstrings.presets[id].relation_roles[role] = member.roles[role];
377+
}
378+
}
379+
}
366380
});
367381

368382
if (listReusedIcons) {
@@ -462,8 +476,10 @@ function generateTranslations(fields, presets, tstrings, searchableFieldIDs) {
462476
let tags = preset.tags || {};
463477
let keys = Object.keys(tags);
464478

479+
const tagsString = keys.map(k => `${k}=${tags[k]}`).join(' + ');
480+
465481
if (keys.length) {
466-
yamlPreset['#name'] = keys.map(k => `${k}=${tags[k]}`).join(' + ');
482+
yamlPreset['#name'] = tagsString;
467483
if (yamlPreset.aliases) {
468484
yamlPreset['#name'] += ' | ' + yamlPreset.aliases.split('\n').join(', ');
469485
}
@@ -474,6 +490,12 @@ function generateTranslations(fields, presets, tstrings, searchableFieldIDs) {
474490
yamlPreset['#name'] += ` | Local preset for countries ${preset.locationSet.include.map(country => `"${country.toUpperCase()}"`).join(', ')}`;
475491
}
476492

493+
if (yamlPreset.relation_roles) {
494+
for (const role in yamlPreset.relation_roles) {
495+
yamlPreset.relation_roles[`#${role}`] = `Relation role “${role}” when used with ${tagsString}`;
496+
}
497+
}
498+
477499
if (preset.searchable !== false) {
478500
if (yamlPreset.terms) {
479501
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": {
@@ -376,5 +375,11 @@
376375
{ "required": ["keys"] }
377376
]}
378377
]}
379-
]
378+
],
379+
"$defs": {
380+
"Geometry": {
381+
"type": "string",
382+
"enum": ["point", "vertex", "line", "area", "relation"]
383+
}
384+
}
380385
}

schemas/preset.json

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,88 @@
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",
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+
"optionalTags": {
155+
"type": "object",
156+
"description": "Only useful for specifying placeholders which are referenced in members.*.matchTags",
157+
"examples": [{ "route": "$1" }],
158+
"additionalProperties": {
159+
"type": "string"
160+
}
161+
},
162+
"id": {
163+
"type": "string",
164+
"description": "The “permanent relation type ID”, this should match the value of https://osm.wiki/Property:P41 in the OSM wiki’s wikibase system."
165+
},
166+
"allowDuplicateMembers": {
167+
"type": "boolean",
168+
"description": "Set to `true` if the same OSM feature is allowed to appear multiple times in the relation's members. The."
169+
},
170+
"members": {
171+
"type": "array",
172+
"items": {
173+
"type": "object",
174+
"properties": {
175+
"role": {
176+
"type": "string",
177+
"description": "The relation role. An empty string is allowed."
178+
},
179+
"roleLabel": {
180+
"type": "string",
181+
"description": "The label for the role, in the default language."
182+
},
183+
"geometry": {
184+
"type": "array",
185+
"items": {
186+
"$ref": "field.json#/$defs/Geometry"
187+
},
188+
"description": "If not specified, any geometry is allowed"
189+
},
190+
"matchTags": {
191+
"type": "array",
192+
"items": {
193+
"type": "object",
194+
"additionalProperties": {
195+
"type": "string"
196+
}
197+
},
198+
"examples": [
199+
[{ "a": 1, "b": 2 }],
200+
[{ "a": 1 }, { "b": 2 }]
201+
],
202+
"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"
203+
},
204+
"min": {
205+
"type": "integer",
206+
"description": "If unspecified, there is no minimum"
207+
},
208+
"max": {
209+
"type": "integer",
210+
"description": "If unspecified, there is no maximum"
211+
}
212+
},
213+
"required": ["role", "roleLabel"],
214+
"additionalProperties": false
215+
}
216+
}
217+
},
218+
"required": ["id", "allowDuplicateMembers", "members"],
219+
"additionalProperties": false
220+
}
221+
}
142222
}

0 commit comments

Comments
 (0)