Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ data/

The format for each file is defined in the [`schemas`](schemas) directory.

#### Preset categories

Files in `data/preset_categories/` (the `categories/` folder in the tree above) define preset groups in the editor. Each JSON file requires `name`, `icon`, and `members`. See the [icons subpage](ICONS.md) for icon ids.

### Presets

A [preset](https://wiki.openstreetmap.org/wiki/Preset) represents a specific type of
Expand Down Expand Up @@ -329,6 +333,10 @@ This can be overwritten by adding the field explicitly like `"fields": [ "shop",

An icon representing a preset, e.g. `"icon": "temaki-power_tower"` ([Example](https://github.com/openstreetmap/id-tagging-schema/blob/main/data/presets/power/tower.json)). More information about available icon sets and usage of icons can be found on the [icons subpage](ICONS.md).

You may set `"icon": "{presets/<preset-id>}"`, e.g. `{presets/shop/books}` for `data/presets/shop/books.json` (or `_books.json`, see [`searchable`](#searchable)). Only `presets/*` is allowed (not `fields/*`). Chains are OK and will also get resolved during build.

For icons on each combo or radio option, see [`icons`](#icons).

##### `imageURL`

The URL of a remote image file. This does not fully replace `icon`—both may be shown in the UI.
Expand Down Expand Up @@ -729,7 +737,11 @@ For `identifier` fields, the regular expression that valid values are expected t

##### `icons`

For combo and radio fields, the `icons` object might contain the name of icons which represent the different values of the field. More information about available icon sets and usage of icons can be found on the [icons subpage](ICONS.md).
On [combo / dropdown](#combodropdown-fields) and [radio](#radio-buttons) fields, the `icons` object maps each option key to an icon id shown beside that value in the editor, e.g. `"zebra": "iD-crossing_markings-zebra"` in the snippet below. More information about available icon sets and usage of icons can be found on the [icons subpage](ICONS.md).

You may set each value to `"{presets/<preset-id>}"` instead of a literal id, with the same rules as [preset `icon`](#icon).

To copy an entire `icons` map from another field, use [`iconsCrossReference`](#iconscrossreference).

Combo field types can accept key-label pairs in the `options` value of the `strings` property.

Expand All @@ -739,7 +751,7 @@ Combo field types can accept key-label pairs in the `options` value of the `stri
"type": "combo",
"label": "Crossing Markings",
"icons": {
"zebra": "iD-crossing_markings-zebra",
"zebra": "{presets/highway/footway/crossing/zebra}",
"lines": "iD-crossing_markings-lines",
}
Expand All @@ -748,7 +760,9 @@ Combo field types can accept key-label pairs in the `options` value of the `stri

##### `iconsCrossReference`

An optional property to reference to the icons of another field, indicated by using that field's name contained in brackets, like `{field}`. This is for example useful when there are multiple variants of fields for the same tag, which should all use the same icons.
An optional property to copy the entire `icons` object from another field by giving that field's id in brackets (no `presets/` prefix)—for example `{kerb}` copies the field whose id is `kerb`. Useful when several field variants share the same option icons.

If a field uses `iconsCrossReference`, the builder copies the other field’s `icons` map first, then expands every `{presets/…}` value in **all** field `icons` maps (including the copy).

### Deprecations

Expand Down
95 changes: 53 additions & 42 deletions lib/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ function processData(options, type) {
let fields = generateFields(dataDir, tstrings, searchableFieldIDs, references);
if (options.processFields) options.processFields(fields);

let presets = generatePresets(dataDir, tstrings, searchableFieldIDs, options.listReusedIcons, references);
let presets = generatePresets(dataDir, tstrings, searchableFieldIDs, references);
if (options.processPresets) options.processPresets(presets);

// Additional consistency checks
Expand All @@ -131,6 +131,8 @@ function processData(options, type) {

dereferenceUntranslatedContent(presets, fields);

reportReusedIcons(presets, options.listReusedIcons);

const defaults = read(dataDir + '/preset_defaults.json');
if (defaults) {
validateSchema(dataDir + '/preset_defaults.json', defaults, defaultsSchema);
Expand Down Expand Up @@ -338,10 +340,57 @@ function stripLeadingUnderscores(str) {
}


function generatePresets(dataDir, tstrings, searchableFieldIDs, listReusedIcons, references) {
let presets = {};
/**
* @param {Record<string, object>} presets
* @param {boolean | number} listReusedIcons
*/
function reportReusedIcons(presets, listReusedIcons) {
if (!listReusedIcons) return;

const icons = {};
for (const id in presets) {
const preset = presets[id];
if (preset.searchable !== false) {
const icon = preset.icon || '(none)';
if (!icons[icon]) icons[icon] = [];
icons[icon].push(id);
}
}

const reuseLimit = typeof listReusedIcons === 'number' && listReusedIcons > 0 ? listReusedIcons : 1;

let icons = {};
let reusedIconPresetCount = 0;
const reusedIcons = Object.keys(icons).filter(function(iconID) {
const presetIDs = icons[iconID];
if (presetIDs.length > reuseLimit) {
reusedIconPresetCount += presetIDs.length;
return true;
}
return false;
});

if (reusedIcons.length > 0) {
process.stdout.write(reusedIcons.length + ' icon(s), including (none), are each used more than ' + reuseLimit + ' time(s), affecting ' + reusedIconPresetCount + ' presets\n');

reusedIcons.sort(function(iconID1, iconID2) {
return icons[iconID2].length - icons[iconID1].length;

}).forEach(function(iconID) {
const presetIDs = icons[iconID];
process.stdout.write(iconID + ', ' + presetIDs.length + '\n');
for (let i in presetIDs) {
process.stdout.write('-' + presetIDs[i] + '\n');
}
process.stdout.write('\n');
});
} else {
process.stdout.write(styleText('green', 'No icon is used more than ' + reuseLimit + ' time(s) across all searchable presets\n'));
}
}


function generatePresets(dataDir, tstrings, searchableFieldIDs, references) {
let presets = {};

fs.globSync(dataDir + '/presets/**/*.json', {
posix: true,
Expand Down Expand Up @@ -398,46 +447,8 @@ function generatePresets(dataDir, tstrings, searchableFieldIDs, listReusedIcons,
}

presets[id] = preset;

if (preset.searchable !== false) {
let icon = preset.icon || '(none)';
if (!icons[icon]) icons[icon] = [];
icons[icon].push(id);
}
});

if (listReusedIcons) {
const reuseLimit = typeof listReusedIcons === 'number' && listReusedIcons > 0 ? listReusedIcons : 1;

let reusedIconPresetCount = 0;
const reusedIcons = Object.keys(icons).filter(function(iconID) {
const presetIDs = icons[iconID];
if (presetIDs.length > reuseLimit) {
reusedIconPresetCount += presetIDs.length;
return true;
}
return false;
});

if (reusedIcons.length > 0) {
process.stdout.write(reusedIcons.length + ' icon(s), including (none), are each used more than ' + reuseLimit + ' time(s), affecting ' + reusedIconPresetCount + ' presets\n');

reusedIcons.sort(function(iconID1, iconID2) {
return icons[iconID2].length - icons[iconID1].length;

}).forEach(function(iconID) {
const presetIDs = icons[iconID];
process.stdout.write(iconID + ', ' + presetIDs.length + '\n');
for (let i in presetIDs) {
process.stdout.write('-' + presetIDs[i] + '\n');
}
process.stdout.write('\n');
});
} else {
process.stdout.write(styleText('green', 'No icon is used more than ' + reuseLimit + ' time(s) across all searchable presets\n'));
}
}

return presets;
}

Expand Down
105 changes: 105 additions & 0 deletions lib/references.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,112 @@ export function isReference(string) {
return string.startsWith('{') && string.endsWith('}');
}

/**
* Resolve "{presets/<id>}" to a concrete icon id (following chains). Only
* `presets/` is valid for icons; `{fields/…}` and bare `{id}` throw.
*
* @param {Record<string, { icon?: string }>} presets
* @param {string} ref
* @param {string} contextMessage
* @param {Set<string>} [visitedPresetIds]
* @returns {string}
*/
function resolveIconRefToId(presets, ref, contextMessage, visitedPresetIds = new Set()) {
if (!isReference(ref)) return ref;

const inner = ref.slice(1, -1);
const slashIdx = inner.indexOf('/');
if (slashIdx === -1) {
throw new Error(
`Invalid icon reference “${ref}” in ${contextMessage}: use “{presets/<preset-id>}” (with a “presets/” prefix).`,
);
}

const type = inner.slice(0, slashIdx);
const id = inner.slice(slashIdx + 1);

if (type === 'fields') {
throw new Error(
`Invalid icon reference “${ref}” in ${contextMessage}: icon references must use “{presets/<preset-id>}”, not “{fields/…}”.`,
);
}
if (type !== 'presets') {
throw new Error(
`Invalid icon reference “${ref}” in ${contextMessage}: only “{presets/<preset-id>}” is allowed (unexpected prefix “${type}/”).`,
);
}
if (!id) {
throw new Error(
`Invalid icon reference “${ref}” in ${contextMessage}: missing preset id after “presets/”.`,
);
}

if (visitedPresetIds.has(id)) {
throw new Error(
`Cycle detected while resolving icon reference “${ref}” in ${contextMessage}: preset “${id}” appears more than once in the chain.`,
);
}
visitedPresetIds.add(id);

const referenced = presets[id];
if (!referenced) {
throw new Error(
`Cannot resolve icon reference “${ref}” in ${contextMessage}: there is no preset “${id}”.`,
);
}

const next = referenced.icon;
if (next === undefined || next === null || next === '') {
throw new Error(
`Cannot resolve icon reference “${ref}” in ${contextMessage}: preset “${id}” has no “icon” property.`,
);
}

if (isReference(next)) {
return resolveIconRefToId(presets, next, contextMessage, visitedPresetIds);
}
return next;
}

/**
* @param {Record<string, { icon?: string }>} presets
* @param {Record<string, { icons?: Record<string, string> }>} fields
*/
function dereferencePresetIconStrings(presets, fields) {
for (const presetID in presets) {
const preset = presets[presetID];
if (typeof preset.icon === 'string' && isReference(preset.icon)) {
preset.icon = resolveIconRefToId(
presets,
preset.icon,
`preset “${presetID}” icon`,
);
}
}

for (const fieldID in fields) {
const field = fields[fieldID];
if (!field.icons || typeof field.icons !== 'object') continue;
for (const key of Object.keys(field.icons)) {
const v = field.icons[key];
if (typeof v === 'string' && isReference(v)) {
field.icons[key] = resolveIconRefToId(
presets,
v,
`field “${fieldID}” icons.${key}`,
);
}
}
}
}

/**
* This is only used to expand references to _untranslated content_.
* For example, `fields` can reference the list of field IDs from another
* preset.
*
* @param {Record<string, object>} presets
* @param {Record<string, object>} fields
*/
export function dereferenceUntranslatedContent(presets, fields) {
for (const presetID in presets) {
Expand Down Expand Up @@ -98,6 +200,9 @@ export function dereferenceUntranslatedContent(presets, fields) {
delete field.locationSetCrossReference;
}
}

// 11. preset `icon` and field `icons` values may use "{presets/<id>}" for another preset's icon id
dereferencePresetIconStrings(presets, fields);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions schemas/field.json
Original file line number Diff line number Diff line change
Expand Up @@ -359,15 +359,15 @@
"enum": ["preset", "changeset", "manual", "group"]
},
"icons": {
"description": "For combo and radio fields: Name of icons which represents different values of this field",
"description": "For combo and radio fields: Name of icons which represents different values of this field. Values may be icon ids or \"{presets/<preset-id>}\".",
"type": "object",
"minProperties": 1,
"additionalProperties": {
"type": "string"
}
},
"iconsCrossReference": {
"description": "A field can reference icons of another by using that field's identifier contained in brackets, like {field}.",
"description": "Copy the entire icons map from another field using that field's id in brackets, e.g. {other_field}.",
"type": "string"
}
},
Expand Down
2 changes: 1 addition & 1 deletion schemas/preset.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
}
},
"icon": {
"description": "Name of preset icon which represents this preset",
"description": "Name of preset icon which represents this preset, or cross-reference with \"{presets/<preset-id>}\".",
"type": "string"
},
"imageURL": {
Expand Down
2 changes: 1 addition & 1 deletion schemas/preset_category.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"type": "string"
},
"icon": {
"description": "Name of preset icon which represents this preset",
"description": "Name of preset icon which represents this category",
"type": "string"
},
"members": {
Expand Down
Loading