scripts/build_data.js (200 lines of code) (raw):
/* eslint-disable no-console */
import chalk from 'chalk';
import fs from 'node:fs';
import stringify from 'json-stringify-pretty-compact';
import shell from 'shelljs';
import YAML from 'js-yaml';
import fetch from 'node-fetch';
import * as languageNames from './language_names.js';
// fontawesome icons
import fontawesome from '@fortawesome/fontawesome-svg-core';
import { fas } from '@fortawesome/free-solid-svg-icons';
import { far } from '@fortawesome/free-regular-svg-icons';
import { fab } from '@fortawesome/free-brands-svg-icons';
import territoryInfo from 'cldr-core/supplemental/territoryInfo.json' assert {type: 'json'};
fontawesome.library.add(fas, far, fab);
let _currBuild = null;
// if called directly, do the thing.
if (process.argv[1].indexOf('build_data.js') > -1) {
buildData();
} else {
module.exports = buildData;
}
function buildData() {
if (_currBuild) return _currBuild;
const START = '🏗 ' + chalk.yellow('Building data...');
const END = '👍 ' + chalk.green('data built');
console.log('');
console.log(START);
console.time(END);
// Create symlinks if necessary.. { 'target': 'source' }
const symlinks = {
'land.html': 'dist/land.html',
img: 'dist/img'
};
for (let target of Object.keys(symlinks)) {
if (!shell.test('-L', target)) {
console.log(`Creating symlink: ${target} -> ${symlinks[target]}`);
shell.ln('-sf', symlinks[target], target);
}
}
// Start clean
shell.rm('-f', [
'data/territory_languages.json',
'dist/locales/en.json',
'dist/data/*',
'svg/fontawesome/*.svg',
]);
// compile Font Awesome icons
let faIcons = new Set([
// list here the icons we want to use in the UI that aren't tied to other data
'fas-filter',
'fas-i-cursor',
'fas-lock',
'fas-palette',
'fas-th-list',
'fas-user-cog'
]);
// add icons for QA integrations
readQAIssueIcons(faIcons);
let territoryLanguages = generateTerritoryLanguages();
fs.writeFileSync('data/territory_languages.json', stringify(territoryLanguages, { maxLength: 9999 }) );
writeEnJson();
const languageInfo = languageNames.langNamesInNativeLang();
fs.writeFileSync('data/languages.json', stringify(languageInfo, { maxLength: 200 }));
fs.writeFileSync('dist/data/languages.min.json', JSON.stringify(languageInfo));
// Save individual data files
let tasks = [
minifyJSON('data/address_formats.json', 'dist/data/address_formats.min.json'),
minifyJSON('data/imagery.json', 'dist/data/imagery.min.json'),
minifyJSON('data/intro_graph.json', 'dist/data/intro_graph.min.json'),
minifyJSON('data/intro_rapid_graph.json', 'dist/data/intro_rapid_graph.min.json'),
minifyJSON('data/keepRight.json', 'dist/data/keepRight.min.json'),
minifyJSON('data/languages.json', 'dist/data/languages.min.json'),
minifyJSON('data/phone_formats.json', 'dist/data/phone_formats.min.json'),
minifyJSON('data/qa_data.json', 'dist/data/qa_data.min.json'),
minifyJSON('data/shortcuts.json', 'dist/data/shortcuts.min.json'),
minifyJSON('data/territory_languages.json', 'dist/data/territory_languages.min.json'),
// writeRapidConfig(),
Promise.all([
// Fetch the icons that are needed by the expected tagging schema version
fetch('https://cdn.jsdelivr.net/npm/@openstreetmap/id-tagging-schema@3/dist/presets.min.json'),
fetch('https://cdn.jsdelivr.net/npm/@openstreetmap/id-tagging-schema@3/dist/preset_categories.min.json'),
fetch('https://cdn.jsdelivr.net/npm/@openstreetmap/id-tagging-schema@3/dist/fields.min.json'),
// WARNING: we fetch the bleeding edge data too to make sure we're always hosting the
// latest icons, but note that the format could break at any time
fetch('https://raw.githubusercontent.com/openstreetmap/id-tagging-schema/main/dist/presets.min.json'),
fetch('https://raw.githubusercontent.com/openstreetmap/id-tagging-schema/main/dist/preset_categories.min.json'),
fetch('https://raw.githubusercontent.com/openstreetmap/id-tagging-schema/main/dist/fields.min.json')
])
.then(responses => Promise.all(responses.map(response => response.json())))
.then((results) => {
// compile the icons used by all the presets
results.forEach(function(data) {
for (var key in data) {
var datum = data[key];
// fontawesome icon
if (datum.icon && /^fa[srb]-/.test(datum.icon)) {
faIcons.add(datum.icon);
}
}
});
// copy over only those Font Awesome icons that we need
writeFaIcons(faIcons);
})
];
return _currBuild =
Promise.all(tasks)
.then(() => {
console.timeEnd(END);
console.log('');
_currBuild = null;
})
.catch((err) => {
console.error(err);
console.log('');
_currBuild = null;
process.exit(1);
});
}
function readQAIssueIcons(faIcons) {
const qa = JSON.parse(fs.readFileSync('data/qa_data.json', 'utf8'));
for (const service in qa) {
for (const item in qa[service].icons) {
const icon = qa[service].icons[item];
// fontawesome icon, remember for later
if (/^fa[srb]-/.test(icon)) {
faIcons.add(icon);
}
}
}
}
function generateTerritoryLanguages() {
let allRawInfo = territoryInfo.supplemental;
let territoryLanguages = {};
Object.keys(allRawInfo).forEach(territoryCode => {
let territoryLangInfo = allRawInfo[territoryCode].languagePopulation;
if (!territoryLangInfo) return;
let langCodes = Object.keys(territoryLangInfo);
territoryLanguages[territoryCode.toLowerCase()] = langCodes.sort((langCode1, langCode2) => {
let popPercent1 = parseFloat(territoryLangInfo[langCode1]._populationPercent);
let popPercent2 = parseFloat(territoryLangInfo[langCode2]._populationPercent);
if (popPercent1 === popPercent2) {
return langCode1.localeCompare(langCode2, 'en', { sensitivity: 'base' });
}
return popPercent2 - popPercent1;
}).map(langCode => langCode.replace('_', '-'));
});
return territoryLanguages;
}
function writeEnJson() {
const readCoreYaml = fs.readFileSync('data/core.yaml', 'utf8');
const readImagery = fs.readFileSync('node_modules/editor-layer-index/i18n/en.yaml', 'utf8');
const readCommunity = fs.readFileSync('node_modules/osm-community-index/i18n/en.yaml', 'utf8');
const readManualImagery = fs.readFileSync('data/manual_imagery.json', 'utf8');
return Promise.all([readCoreYaml, readImagery, readCommunity, readManualImagery])
.then(data => {
let core = YAML.load(data[0]);
let imagery = YAML.load(data[1]);
let community = YAML.load(data[2]);
let manualImagery = JSON.parse(data[3]);
for (let i in manualImagery) {
let layer = manualImagery[i];
let id = layer.id;
for (let key in layer) {
if (key === 'attribution') {
for (let attrKey in layer[key]) {
if (attrKey !== 'text') {
delete layer[key][attrKey];
}
}
} else if (['name', 'description'].indexOf(key) === -1) {
delete layer[key];
}
}
// tack on strings for additional imagery not included in the index
imagery.en.imagery[id] = layer;
}
let enjson = core;
let props = ['imagery', 'community', 'languageNames', 'scriptNames'];
props.forEach(function(prop) {
if (enjson.en[prop]) {
console.error(`Error: Reserved property '${prop}' already exists in core strings`);
process.exit(1);
}
});
enjson.en.imagery = imagery.en.imagery;
enjson.en.community = community.en;
enjson.en.languageNames = languageNames.languageNamesInLanguageOf('en');
enjson.en.scriptNames = languageNames.scriptNamesInLanguageOf('en');
fs.writeFileSync('dist/locales/en.min.json', JSON.stringify(enjson));
});
}
function writeFaIcons(faIcons) {
Array.from(faIcons).forEach(function(key) {
const prefix = key.substring(0, 3); // `fas`, `far`, `fab`
const name = key.substring(4);
const def = fontawesome.findIconDefinition({ prefix: prefix, iconName: name });
try {
fs.writeFileSync(`svg/fontawesome/${key}.svg`, fontawesome.icon(def).html.toString());
} catch (error) {
console.error(`Error: No FontAwesome icon for ${key}`);
throw (error);
}
});
}
function minifyJSON(inPath, outPath) {
return new Promise((resolve, reject) => {
fs.readFile(inPath, 'utf8', (err, data) => {
if (err) return reject(err);
const minified = JSON.stringify(JSON.parse(data));
fs.writeFile(outPath, minified, (err) => {
if (err) return reject(err);
resolve();
});
});
});
}
export default buildData;