From 2967d2d020da37848d47e8fdc26031a23d63671d Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 16 Apr 2019 18:06:05 -0700 Subject: [PATCH] Add support for icons from the noun project (close #5691) --- .gitignore | 1 + build_data.js | 85 +++++++++++++++++-- data/presets/README.md | 26 +++--- data/presets/presets.json | 2 +- .../presets/landuse/military/airfield.json | 2 +- data/taginfo.json | 2 +- modules/svg/defs.js | 2 +- modules/ui/preset_icon.js | 3 +- package.json | 1 + svg/the-noun-project/2009265.svg | 1 + 10 files changed, 104 insertions(+), 21 deletions(-) create mode 100644 svg/the-noun-project/2009265.svg diff --git a/.gitignore b/.gitignore index c293e302e..6e679e097 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ /.tx/tmp/ npm-debug.log package-lock.json +the_noun_project.auth transifex.auth # autogenerated symlinks diff --git a/build_data.js b/build_data.js index ac77f7314..dac77d01a 100644 --- a/build_data.js +++ b/build_data.js @@ -21,6 +21,16 @@ const far = require('@fortawesome/free-regular-svg-icons').far; const fab = require('@fortawesome/free-brands-svg-icons').fab; fontawesome.library.add(fas, far, fab); +/* + * The Noun Project doesn't allow anonymous API access. New "tnp-" icons will + * not be downloaded without a the_noun_project.auth file with a json object: + * { + * "consumer_key": "xxxxxx", + * "consumer_secret": "xxxxxx" + * } + * */ +const nounAuth = JSON.parse(fs.readFileSync('./the_noun_project.auth', 'utf8')); +const request = require('request').defaults({ maxSockets: 1 }); module.exports = function buildData() { var building; @@ -57,6 +67,8 @@ module.exports = function buildData() { 'fas-long-arrow-alt-right': {} }; + var tnpIcons = {}; + // Start clean shell.rm('-f', [ 'data/presets/categories.json', @@ -68,9 +80,9 @@ module.exports = function buildData() { 'svg/fontawesome/*.svg', ]); - var categories = generateCategories(tstrings, faIcons); + var categories = generateCategories(tstrings, faIcons, tnpIcons); var fields = generateFields(tstrings, faIcons); - var presets = generatePresets(tstrings, faIcons); + var presets = generatePresets(tstrings, faIcons, tnpIcons); var defaults = read('data/presets/defaults.json'); var translations = generateTranslations(fields, presets, tstrings); var taginfo = generateTaginfo(presets, fields); @@ -103,7 +115,8 @@ module.exports = function buildData() { prettyStringify(taginfo, { maxLength: 9999 }) ), writeEnJson(tstrings), - writeFaIcons(faIcons) + writeFaIcons(faIcons), + writeTnpIcons(tnpIcons) ]; return Promise.all(tasks) @@ -140,7 +153,7 @@ function validate(file, instance, schema) { } -function generateCategories(tstrings, faIcons) { +function generateCategories(tstrings, faIcons, tnpIcons) { var categories = {}; glob.sync(__dirname + '/data/presets/categories/*.json').forEach(function(file) { var category = read(file); @@ -152,12 +165,16 @@ function generateCategories(tstrings, faIcons) { if (/^fa[srb]-/.test(category.icon)) { faIcons[category.icon] = {}; } + // noun project icon, remember for later + if (/^tnp-/.test(category.icon)) { + tnpIcons[category.icon] = {}; + } }); return categories; } -function generateFields(tstrings, faIcons) { +function generateFields(tstrings, faIcons, tnpIcons) { var fields = {}; glob.sync(__dirname + '/data/presets/fields/**/*.json').forEach(function(file) { var field = read(file); @@ -185,6 +202,10 @@ function generateFields(tstrings, faIcons) { if (/^fa[srb]-/.test(field.icon)) { faIcons[field.icon] = {}; } + // noun project icon, remember for later + if (/^tnp-/.test(field.icon)) { + tnpIcons[field.icon] = {}; + } }); return fields; } @@ -270,7 +291,7 @@ function stripLeadingUnderscores(str) { } -function generatePresets(tstrings, faIcons) { +function generatePresets(tstrings, faIcons, tnpIcons) { var presets = {}; glob.sync(__dirname + '/data/presets/presets/**/*.json').forEach(function(file) { @@ -290,6 +311,10 @@ function generatePresets(tstrings, faIcons) { if (/^fa[srb]-/.test(preset.icon)) { faIcons[preset.icon] = {}; } + // noun project icon, remember for later + if (/^tnp-/.test(preset.icon)) { + tnpIcons[preset.icon] = {}; + } }); presets = Object.assign(presets, suggestionsToPresets(presets)); @@ -401,6 +426,9 @@ function generateTaginfo(presets, fields) { } else if (/^iD-/.test(preset.icon)) { tag.icon_url = 'https://raw.githubusercontent.com/openstreetmap/iD/master/svg/iD-sprite/presets/' + preset.icon.replace(/^iD-/, '') + '.svg?sanitize=true'; + } else if (/^tnp-/.test(preset.icon)) { + tag.icon_url = 'https://raw.githubusercontent.com/openstreetmap/iD/master/svg/the-noun-project/' + + preset.icon.replace(/^tnp-/, '') + '.svg?sanitize=true'; } coalesceTags(taginfo, tag); @@ -656,6 +684,51 @@ function writeFaIcons(faIcons) { } +function writeTnpIcons(tnpIcons) { + var baseURL = 'http://api.thenounproject.com/icon/'; + for (var key in tnpIcons) { + var id = key.substring(4); + var localPath = 'svg/the-noun-project/' + id + '.svg'; + + // don't redownload existing icons + if (fs.existsSync(localPath)) continue; + + if (!nounAuth) { + console.error('No authentication file found for The Noun Project with which to download the icon with id: ' + key); + return; + } + + var url = baseURL + id; + request.get(url, { oauth : nounAuth }, handleTheNounProjectResponse); + } +} + +function handleTheNounProjectResponse(err, resp, body) { + if (err) { + console.error(err); + return; + } + var icon = JSON.parse(body).icon; + if (icon.license_description !== 'public-domain') { + console.error('The icon ' + icon.term + ' (tnp-' + icon.id + ') from The Noun Project cannot be used in iD because it is not in the public domain.'); + return; + } + var iconURL = icon.icon_url; + request.get(iconURL, function(err2, resp2, svg) { + if (err2) { + console.error(err2); + return; + } + try { + writeFileProm('svg/the-noun-project/' + icon.id + '.svg', svg); + } catch (error) { + console.error(error); + throw (error); + } + }); +} + + function writeFileProm(path, content) { return new Promise(function(res, rej) { fs.writeFile(path, content, function(err) { diff --git a/data/presets/README.md b/data/presets/README.md index 543113be7..649b79d2f 100644 --- a/data/presets/README.md +++ b/data/presets/README.md @@ -127,7 +127,7 @@ Generally, these properties will be equivalent and should be supersets of `tags` iD's validator will recommend that users add missing tags from `addTags` to matching features. -For example, the Bridge preset has these properties: +For example, the Bridge preset has these properties: ``` "tags": { @@ -143,16 +143,22 @@ When adding a feature with this preset, it will be given the tags `man_made=brid ##### `icon` -The name of a local SVG icon file. You can use icons from any of the following open source icon sets. +The name of a local SVG icon file. You can use icons from any of the following icon sets. When specifying an icon, use the prefixed version of the name, for example `"icon": "maki-park"` or `"icon": "tnp-2009223"`. -* [Maki](http://www.mapbox.com/maki/) - prefix: `maki-` -* [Temaki](http://bhousel.github.io/temaki/docs/) - prefix: `temaki-` -* [Font Awesome (free, solid)](https://fontawesome.com/icons?d=gallery&s=solid) - prefix: `fas-` -* [Font Awesome (free, regular)](https://fontawesome.com/icons?d=gallery&s=regular) - prefix: `far-` -* [Font Awesome (free, brands)](https://fontawesome.com/icons?d=gallery&s=brands) - prefix: `fab-` -* [iD's spritesheet](https://github.com/openstreetmap/iD/tree/master/svg/iD-sprite/presets) - prefix: `iD-` - -When specifying an icon, use the prefixed version of the name, for example `"icon": "maki-park"`. +* [iD's spritesheet](https://github.com/openstreetmap/iD/tree/master/svg/iD-sprite/presets) (`iD-`) +* [Maki](http://www.mapbox.com/maki/) (`maki-`), map-specific icons from Mapbox +* [Temaki](http://bhousel.github.io/temaki/docs/) (`temaki-`), an expansion pack for Maki + * This is the best place to submit a PR if you want to create a preset icon! +* [Font Awesome](https://fontawesome.com/icons?d=gallery&m=free), thousands of general-purpose icons + * There is a free and pro tier. You can use any icon from the free tier in the following styles: + * [Solid](https://fontawesome.com/icons?d=gallery&s=solid&m=free) (`fas-`) + * [Regular](https://fontawesome.com/icons?d=gallery&s=regular&m=free) (`far-`) + * [Brands](https://fontawesome.com/icons?d=gallery&s=brands&m=free) (`fab-`) +* [The Noun Project](https://thenounproject.com) (`tnp-`), millions of general-purpose icons + * The licenses vary. You can only use the public-domain icons in iD. + * The icon styles vary. Avoid icons with thin outlines or too much detail since they will not look good at small sizes in iD. + * Use the numeric ID of the icon (e.g. `2009223`). This is shown in the URL when you select an icon on their site. + * Unfortunately, you must [sign up for a free API key](https://thenounproject.com/developers/) in order to download new icons (even though they're public-domain). Add a file called `the_noun_project.auth` to the root of your local iD instance containing your credentials like `{"consumer_key": "xxxxxx", "consumer_secret": "xxxxxx"}`. This file is not version-controlled. ##### `imageURL` diff --git a/data/presets/presets.json b/data/presets/presets.json index b38001227..abce48217 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -540,7 +540,7 @@ "landuse/landfill": {"geometry": ["area"], "fields": ["name"], "moreFields": ["address", "website", "phone", "email", "fax"], "tags": {"landuse": "landfill"}, "terms": ["dump"], "name": "Landfill"}, "landuse/meadow": {"icon": "maki-garden", "geometry": ["area"], "fields": ["name"], "tags": {"landuse": "meadow"}, "terms": [], "name": "Meadow"}, "landuse/military": {"icon": "temaki-military", "fields": ["name"], "moreFields": ["address", "website", "phone", "email", "fax"], "geometry": ["area"], "tags": {"landuse": "military"}, "terms": [], "matchScore": 0.9, "name": "Military Area"}, - "landuse/military/airfield": {"icon": "maki-airfield", "fields": ["name", "iata", "icao"], "geometry": ["point", "area"], "tags": {"military": "airfield"}, "addTags": {"aeroway": "aerodrome", "landuse": "military", "military": "airfield"}, "removeTags": {"aeroway": "aerodrome", "landuse": "military", "military": "airfield"}, "reference": {"key": "military", "value": "airfield"}, "terms": ["aerodrome", "aeroway", "air force", "airplane", "airport", "army", "base", "bomb", "fight", "force", "guard", "heli*", "jet", "marine", "navy", "plane", "troop", "war"], "name": "Military Airfield"}, + "landuse/military/airfield": {"icon": "tnp-2009265", "fields": ["name", "iata", "icao"], "geometry": ["point", "area"], "tags": {"military": "airfield"}, "addTags": {"aeroway": "aerodrome", "landuse": "military", "military": "airfield"}, "removeTags": {"aeroway": "aerodrome", "landuse": "military", "military": "airfield"}, "reference": {"key": "military", "value": "airfield"}, "terms": ["aerodrome", "aeroway", "air force", "airplane", "airport", "army", "base", "bomb", "fight", "force", "guard", "heli*", "jet", "marine", "navy", "plane", "troop", "war"], "name": "Military Airfield"}, "landuse/military/barracks": {"icon": "temaki-military", "fields": ["name", "building_area"], "geometry": ["point", "area"], "tags": {"military": "barracks"}, "addTags": {"landuse": "military", "military": "barracks"}, "removeTags": {"landuse": "military", "military": "barracks"}, "terms": ["air force", "army", "base", "fight", "force", "guard", "marine", "navy", "troop", "war"], "name": "Barracks"}, "landuse/military/bunker": {"icon": "temaki-military", "fields": ["name", "bunker_type", "building_area"], "geometry": ["point", "area"], "tags": {"military": "bunker"}, "addTags": {"building": "bunker", "landuse": "military", "military": "bunker"}, "removeTags": {"building": "bunker", "landuse": "military", "military": "bunker"}, "terms": ["air force", "army", "base", "fight", "force", "guard", "marine", "navy", "troop", "war"], "name": "Military Bunker"}, "landuse/military/checkpoint": {"icon": "maki-barrier", "fields": ["name"], "geometry": ["point", "vertex", "area"], "tags": {"military": "checkpoint"}, "addTags": {"landuse": "military", "military": "checkpoint"}, "removeTags": {"landuse": "military", "military": "checkpoint"}, "terms": ["air force", "army", "base", "force", "guard", "marine", "navy", "troop", "war"], "name": "Checkpoint"}, diff --git a/data/presets/presets/landuse/military/airfield.json b/data/presets/presets/landuse/military/airfield.json index 97b61ff02..a2aaaa0b7 100644 --- a/data/presets/presets/landuse/military/airfield.json +++ b/data/presets/presets/landuse/military/airfield.json @@ -1,5 +1,5 @@ { - "icon": "maki-airfield", + "icon": "tnp-2009265", "fields": [ "name", "iata", diff --git a/data/taginfo.json b/data/taginfo.json index 5b2b89517..4530f718e 100644 --- a/data/taginfo.json +++ b/data/taginfo.json @@ -520,7 +520,7 @@ {"key": "landuse", "value": "landfill", "description": "🄿 Landfill", "object_types": ["area"]}, {"key": "landuse", "value": "meadow", "description": "🄿 Meadow", "object_types": ["area"], "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/garden-15.svg?sanitize=true"}, {"key": "landuse", "value": "military", "description": "🄿 Military Area", "object_types": ["area"], "icon_url": "https://raw.githubusercontent.com/bhousel/temaki/master/icons/military.svg?sanitize=true"}, - {"key": "military", "value": "airfield", "description": "🄿 Military Airfield", "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/airfield-15.svg?sanitize=true"}, + {"key": "military", "value": "airfield", "description": "🄿 Military Airfield", "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/the-noun-project/2009265.svg?sanitize=true"}, {"key": "military", "value": "barracks", "description": "🄿 Barracks", "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/bhousel/temaki/master/icons/military.svg?sanitize=true"}, {"key": "military", "value": "bunker", "description": "🄿 Military Bunker", "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/bhousel/temaki/master/icons/military.svg?sanitize=true"}, {"key": "military", "value": "checkpoint", "description": "🄿 Checkpoint", "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/barrier-15.svg?sanitize=true"}, diff --git a/modules/svg/defs.js b/modules/svg/defs.js index fa89f04c6..7e8fb264b 100644 --- a/modules/svg/defs.js +++ b/modules/svg/defs.js @@ -173,7 +173,7 @@ export function svgDefs(context) { // add symbol spritesheets defs .call(drawDefs.addSprites, [ - 'iD-sprite', 'maki-sprite', 'temaki-sprite', 'fa-sprite', 'community-sprite' + 'iD-sprite', 'maki-sprite', 'temaki-sprite', 'fa-sprite', 'tnp-sprite', 'community-sprite' ], true); } diff --git a/modules/ui/preset_icon.js b/modules/ui/preset_icon.js index c7041339e..70ad9984f 100644 --- a/modules/ui/preset_icon.js +++ b/modules/ui/preset_icon.js @@ -206,7 +206,8 @@ export function uiPresetIcon(context) { var isMaki = picon && /^maki-/.test(picon); var isTemaki = picon && /^temaki-/.test(picon); var isFa = picon && /^fa[srb]-/.test(picon); - var isiDIcon = picon && !(isMaki || isTemaki || isFa); + var isTnp = picon && /^tnp-/.test(picon); + var isiDIcon = picon && !(isMaki || isTemaki || isFa || isTnp); var isCategory = !p.setTags; var drawPoint = picon && geom === 'point' && isSmall() && !isFallback; var drawVertex = picon !== null && geom === 'vertex' && (!isSmall() || !isFallback); diff --git a/package.json b/package.json index 7ef10efcf..4eda34080 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dist:svg:id": "svg-sprite --symbol --symbol-dest . --shape-id-generator \"iD-%s\" --symbol-sprite dist/img/iD-sprite.svg \"svg/iD-sprite/**/*.svg\"", "dist:svg:community": "svg-sprite --symbol --symbol-dest . --shape-id-generator \"community-%s\" --symbol-sprite dist/img/community-sprite.svg node_modules/osm-community-index/dist/img/*.svg", "dist:svg:fa": "svg-sprite --symbol --symbol-dest . --symbol-sprite dist/img/fa-sprite.svg svg/fontawesome/*.svg", + "dist:svg:tnp": "svg-sprite --symbol --symbol-dest . --shape-id-generator \"tnp-%s\" --symbol-sprite dist/img/tnp-sprite.svg svg/the-noun-project/*.svg", "dist:svg:maki": "svg-sprite --symbol --symbol-dest . --shape-id-generator \"maki-%s\" --symbol-sprite dist/img/maki-sprite.svg node_modules/@mapbox/maki/icons/*.svg", "dist:svg:mapillary": "svg-sprite --symbol --symbol-dest . --symbol-sprite dist/img/mapillary-sprite.svg node_modules/mapillary_sprite_source/package_signs/*.svg", "dist:svg:temaki": "svg-sprite --symbol --symbol-dest . --shape-id-generator \"temaki-%s\" --symbol-sprite dist/img/temaki-sprite.svg node_modules/temaki/icons/*.svg", diff --git a/svg/the-noun-project/2009265.svg b/svg/the-noun-project/2009265.svg new file mode 100644 index 000000000..20a9d8b9a --- /dev/null +++ b/svg/the-noun-project/2009265.svg @@ -0,0 +1 @@ +Asset 578 \ No newline at end of file