diff --git a/index.css b/index.css index 86cbfaca..8429bba3 100644 --- a/index.css +++ b/index.css @@ -1457,13 +1457,13 @@ div.states > .coaIcon > use { pointer-events: none; } -div.states > .icon { +div.states > .resourceIcon { margin: 0; cursor: pointer; vertical-align: middle; } -div.states > .icon > * { +div.states > .resourceIcon > * { pointer-events: none; } @@ -1548,7 +1548,8 @@ div.states > div.resourceBonus > span.icon-male { #stateNameEditor div.label, #provinceNameEditor div.label, -#regimentBody div.label { +#regimentBody div.label, +#resourceIconEditor div.label { display: inline-block; width: 5.5em; } diff --git a/index.html b/index.html index 402baaec..dd50311e 100644 --- a/index.html +++ b/index.html @@ -208,6 +208,7 @@ + @@ -3031,6 +3032,26 @@ + + + Select: + + + + Upload: + raster + vector + + + Comperess raster + | + Optimize vector + + + + + + @@ -3468,15 +3489,14 @@ Drop a .map file to open - - - - - - - - - + + + + + + + + diff --git a/modules/save-and-load.js b/modules/save-and-load.js index 1d29590f..3022174b 100644 --- a/modules/save-and-load.js +++ b/modules/save-and-load.js @@ -137,6 +137,8 @@ async function getMapURL(type, subtype) { cloneDefs.querySelector('#defs-emblems')?.remove(); } + // add displayed resource icons TODO + // replace ocean pattern href to base64 if (PRODUCTION && cloneEl.getElementById('oceanicPattern')) { const el = cloneEl.getElementById('oceanicPattern'); @@ -1310,6 +1312,7 @@ function parseLoadedData(data) { if (version < 1.7) { // v 1.7 added resources layer goods = viewbox.append('g').attr('id', 'goods'); + defs.append('g').attr('id', 'defs-icons'); } void (function checkDataIntegrity() { diff --git a/modules/ui/emblems-editor.js b/modules/ui/emblems-editor.js index 5b74f58a..579b3565 100644 --- a/modules/ui/emblems-editor.js +++ b/modules/ui/emblems-editor.js @@ -1,20 +1,23 @@ -"use strict"; +'use strict'; function editEmblem(type, id, el) { if (customization) return; if (!id && d3.event) defineEmblemData(d3.event); - emblems.selectAll("use").call(d3.drag().on("drag", dragEmblem)).classed("draggable", true); + emblems.selectAll('use').call(d3.drag().on('drag', dragEmblem)).classed('draggable', true); - const emblemStates = document.getElementById("emblemStates"); - const emblemProvinces = document.getElementById("emblemProvinces"); - const emblemBurgs = document.getElementById("emblemBurgs"); - const emblemShapeSelector = document.getElementById("emblemShapeSelector"); + const emblemStates = document.getElementById('emblemStates'); + const emblemProvinces = document.getElementById('emblemProvinces'); + const emblemBurgs = document.getElementById('emblemBurgs'); + const emblemShapeSelector = document.getElementById('emblemShapeSelector'); updateElementSelectors(type, id, el); - $("#emblemEditor").dialog({ - title: "Edit Emblem", resizable: true, width: "18.2em", height: "auto", - position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"}, + $('#emblemEditor').dialog({ + title: 'Edit Emblem', + resizable: true, + width: '18.2em', + height: 'auto', + position: {my: 'left top', at: 'left+10 top+10', of: 'svg', collision: 'fit'}, close: closeEmblemEditor }); @@ -23,45 +26,45 @@ function editEmblem(type, id, el) { emblemProvinces.oninput = selectProvince; emblemBurgs.oninput = selectBurg; emblemShapeSelector.oninput = changeShape; - document.getElementById("emblemSizeSlider").oninput = changeSize; - document.getElementById("emblemSizeNumber").oninput = changeSize; - document.getElementById("emblemsRegenerate").onclick = regenerate; - document.getElementById("emblemsArmoria").onclick = openInArmoria; - document.getElementById("emblemsUpload").onclick = toggleUpload; - document.getElementById("emblemsUploadImage").onclick = () => emblemImageToLoad.click(); - document.getElementById("emblemsUploadSVG").onclick = () => emblemSVGToLoad.click(); - document.getElementById("emblemImageToLoad").onchange = () => upload("image"); - document.getElementById("emblemSVGToLoad").onchange = () => upload("svg"); - document.getElementById("emblemsDownload").onclick = toggleDownload; - document.getElementById("emblemsDownloadSVG").onclick = () => download("svg"); - document.getElementById("emblemsDownloadPNG").onclick = () => download("png"); - document.getElementById("emblemsDownloadJPG").onclick = () => download("jpeg"); - document.getElementById("emblemsGallery").onclick = downloadGallery; - document.getElementById("emblemsFocus").onclick = showArea; + document.getElementById('emblemSizeSlider').oninput = changeSize; + document.getElementById('emblemSizeNumber').oninput = changeSize; + document.getElementById('emblemsRegenerate').onclick = regenerate; + document.getElementById('emblemsArmoria').onclick = openInArmoria; + document.getElementById('emblemsUpload').onclick = toggleUpload; + document.getElementById('emblemsUploadImage').onclick = () => imageToLoad.click(); + document.getElementById('emblemsUploadSVG').onclick = () => svgToLoad.click(); + document.getElementById('imageToLoad').onchange = () => uploadImage('image'); + document.getElementById('svgToLoad').onchange = () => uploadImage('svg'); + document.getElementById('emblemsDownload').onclick = toggleDownload; + document.getElementById('emblemsDownloadSVG').onclick = () => download('svg'); + document.getElementById('emblemsDownloadPNG').onclick = () => download('png'); + document.getElementById('emblemsDownloadJPG').onclick = () => download('jpeg'); + document.getElementById('emblemsGallery').onclick = downloadGallery; + document.getElementById('emblemsFocus').onclick = showArea; function defineEmblemData(e) { const parent = e.target.parentNode; - const [g, t] = parent.id === "burgEmblems" ? [pack.burgs, "burg"] : - parent.id === "provinceEmblems" ? [pack.provinces, "province"] : - [pack.states, "state"]; + const [g, t] = parent.id === 'burgEmblems' ? [pack.burgs, 'burg'] : parent.id === 'provinceEmblems' ? [pack.provinces, 'province'] : [pack.states, 'state']; const i = +e.target.dataset.i; type = t; - id = type+"COA"+i; + id = type + 'COA' + i; el = g[i]; } function updateElementSelectors(type, id, el) { - let state = 0, province = 0, burg = 0; + let state = 0, + province = 0, + burg = 0; // set active type - emblemStates.parentElement.className = type === "state" ? "active" : ""; - emblemProvinces.parentElement.className = type === "province" ? "active" : ""; - emblemBurgs.parentElement.className = type === "burg" ? "active" : ""; + emblemStates.parentElement.className = type === 'state' ? 'active' : ''; + emblemProvinces.parentElement.className = type === 'province' ? 'active' : ''; + emblemBurgs.parentElement.className = type === 'burg' ? 'active' : ''; // define selected values - if (type === "state") state = el.i; - else if (type === "province") { - province = el.i + if (type === 'state') state = el.i; + else if (type === 'province') { + province = el.i; state = pack.states[el.state].i; } else { burg = el.i; @@ -69,24 +72,24 @@ function editEmblem(type, id, el) { state = el.state; } - const validBurgs = pack.burgs.filter(burg => burg.i && !burg.removed && burg.coa); + const validBurgs = pack.burgs.filter((burg) => burg.i && !burg.removed && burg.coa); // update option list and select actual values emblemStates.options.length = 0; - const neutralBurgs = validBurgs.filter(burg => !burg.state); + const neutralBurgs = validBurgs.filter((burg) => !burg.state); if (neutralBurgs.length) emblemStates.options.add(new Option(pack.states[0].name, 0, false, !state)); - const stateList = pack.states.filter(state => state.i && !state.removed); - stateList.forEach(s => emblemStates.options.add(new Option(s.name, s.i, false, s.i === state))); + const stateList = pack.states.filter((state) => state.i && !state.removed); + stateList.forEach((s) => emblemStates.options.add(new Option(s.name, s.i, false, s.i === state))); emblemProvinces.options.length = 0; - emblemProvinces.options.add(new Option("", 0, false, !province)); - const provinceList = pack.provinces.filter(province => !province.removed && province.state === state); - provinceList.forEach(p => emblemProvinces.options.add(new Option(p.name, p.i, false, p.i === province))); + emblemProvinces.options.add(new Option('', 0, false, !province)); + const provinceList = pack.provinces.filter((province) => !province.removed && province.state === state); + provinceList.forEach((p) => emblemProvinces.options.add(new Option(p.name, p.i, false, p.i === province))); emblemBurgs.options.length = 0; - emblemBurgs.options.add(new Option("", 0, false, !burg)); - const burgList = validBurgs.filter(burg => province ? pack.cells.province[burg.cell] === province : burg.state === state); - burgList.forEach(b => emblemBurgs.options.add(new Option(b.capital ? "👑 " + b.name : b.name, b.i, false, b.i === burg))); + emblemBurgs.options.add(new Option('', 0, false, !burg)); + const burgList = validBurgs.filter((burg) => (province ? pack.cells.province[burg.cell] === province : burg.state === state)); + burgList.forEach((b) => emblemBurgs.options.add(new Option(b.capital ? '👑 ' + b.name : b.name, b.i, false, b.i === burg))); emblemBurgs.options[0].disabled = true; COArenderer.trigger(id, el.coa); @@ -95,35 +98,35 @@ function editEmblem(type, id, el) { function updateEmblemData(type, id, el) { if (!el.coa) return; - document.getElementById("emblemImage").setAttribute("href", "#" + id); + document.getElementById('emblemImage').setAttribute('href', '#' + id); let name = el.fullName || el.name; - if (type === "burg") name = "Burg of " + name; - document.getElementById("emblemArmiger").innerText = name; + if (type === 'burg') name = 'Burg of ' + name; + document.getElementById('emblemArmiger').innerText = name; - if (el.coa === "custom") emblemShapeSelector.disabled = true; + if (el.coa === 'custom') emblemShapeSelector.disabled = true; else { emblemShapeSelector.disabled = false; emblemShapeSelector.value = el.coa.shield; } const size = el.coaSize || 1; - document.getElementById("emblemSizeSlider").value = size; - document.getElementById("emblemSizeNumber").value = size; + document.getElementById('emblemSizeSlider').value = size; + document.getElementById('emblemSizeNumber').value = size; } function selectState() { const state = +this.value; if (state) { - type = "state"; + type = 'state'; el = pack.states[state]; - id = "stateCOA"+ state; + id = 'stateCOA' + state; } else { // select neutral burg if state is changed to Neutrals - const neutralBurgs = pack.burgs.filter(burg => burg.i && !burg.removed && !burg.state); + const neutralBurgs = pack.burgs.filter((burg) => burg.i && !burg.removed && !burg.state); if (!neutralBurgs.length) return; - type = "burg"; + type = 'burg'; el = neutralBurgs[0]; - id = "burgCOA"+ neutralBurgs[0].i; + id = 'burgCOA' + neutralBurgs[0].i; } updateElementSelectors(type, id, el); } @@ -132,15 +135,15 @@ function editEmblem(type, id, el) { const province = +this.value; if (province) { - type = "province"; + type = 'province'; el = pack.provinces[province]; - id = "provinceCOA"+ province; + id = 'provinceCOA' + province; } else { // select state if province is changed to null value const state = +emblemStates.value; - type = "state"; + type = 'state'; el = pack.states[state]; - id = "stateCOA"+ state; + id = 'stateCOA' + state; } updateElementSelectors(type, id, el); @@ -148,9 +151,9 @@ function editEmblem(type, id, el) { function selectBurg() { const burg = +this.value; - type = "burg"; + type = 'burg'; el = pack.burgs[burg]; - id = "burgCOA"+ burg; + id = 'burgCOA' + burg; updateElementSelectors(type, id, el); } @@ -166,35 +169,38 @@ function editEmblem(type, id, el) { } function changeSize() { - const size = el.coaSize = +this.value; - document.getElementById("emblemSizeSlider").value = size; - document.getElementById("emblemSizeNumber").value = size; + const size = (el.coaSize = +this.value); + document.getElementById('emblemSizeSlider').value = size; + document.getElementById('emblemSizeNumber').value = size; - const g = emblems.select("#"+type+"Emblems"); - g.select("[data-i='"+el.i+"']").remove(); + const g = emblems.select('#' + type + 'Emblems'); + g.select("[data-i='" + el.i + "']").remove(); if (!size) return; // re-append use element - const categotySize = +g.attr("font-size"); - const shift = categotySize * size / 2; + const categotySize = +g.attr('font-size'); + const shift = (categotySize * size) / 2; const x = el.x || el.pole[0]; const y = el.y || el.pole[1]; - g.append("use").attr("data-i", el.i) - .attr("x", rn(x - shift), 2).attr("y", rn(y - shift), 2) - .attr("width", size+"em").attr("height", size+"em") - .attr("href", "#"+id); + g.append('use') + .attr('data-i', el.i) + .attr('x', rn(x - shift), 2) + .attr('y', rn(y - shift), 2) + .attr('width', size + 'em') + .attr('height', size + 'em') + .attr('href', '#' + id); } function regenerate() { let parent = null; - if (type === "province") parent = pack.states[el.state]; - else if (type === "burg") { + if (type === 'province') parent = pack.states[el.state]; + else if (type === 'burg') { const province = pack.cells.province[el.cell]; parent = province ? pack.provinces[province] : pack.states[el.state]; } const shield = el.coa.shield || COA.getShield(el.culture || parent?.culture || 0, el.state); - el.coa = COA.generate(parent ? parent.coa : null, .3, .1, null); + el.coa = COA.generate(parent ? parent.coa : null, 0.3, 0.1, null); el.coa.shield = shield; emblemShapeSelector.disabled = false; emblemShapeSelector.value = el.coa.shield; @@ -205,85 +211,80 @@ function editEmblem(type, id, el) { } function openInArmoria() { - const coa = el.coa && el.coa !== "custom" ? el.coa : {t1: "sable"}; - const json = JSON.stringify(coa).replaceAll("#", "%23"); + const coa = el.coa && el.coa !== 'custom' ? el.coa : {t1: 'sable'}; + const json = JSON.stringify(coa).replaceAll('#', '%23'); const url = `https://azgaar.github.io/Armoria/?coa=${json}&from=FMG`; openURL(url); } function toggleUpload() { - document.getElementById("emblemDownloadControl").classList.add("hidden"); - const buttons = document.getElementById("emblemUploadControl"); - buttons.classList.toggle("hidden"); + document.getElementById('emblemDownloadControl').classList.add('hidden'); + const buttons = document.getElementById('emblemUploadControl'); + buttons.classList.toggle('hidden'); } - function upload(type) { - const input = type === "image" ? document.getElementById("emblemImageToLoad") : document.getElementById("emblemSVGToLoad"); + function uploadImage(type) { + const input = type === 'image' ? document.getElementById('imageToLoad') : document.getElementById('svgToLoad'); const file = input.files[0]; - input.value = ""; + input.value = ''; - if (file.size > 500000) { - tip(`File is too big, please optimize file size up to 500kB and re-upload. Recommended size is 200x200 px and up to 100kB`, true, "error", 5000); - return; - } + if (file.size > 500000) return tip(`File is too big, please optimize file size up to 500kB and re-upload. Recommended size is 200x200 px and up to 100kB`, true, 'error', 5000); const reader = new FileReader(); - - reader.onload = function(readerEvent) { + reader.onload = function (readerEvent) { const result = readerEvent.target.result; - const defs = document.getElementById("defs-emblems"); + const defs = document.getElementById('defs-emblems'); const coa = document.getElementById(id); // old emblem - if (type === "image") { + if (type === 'image') { const svg = ``; - defs.insertAdjacentHTML("beforeend", svg); + defs.insertAdjacentHTML('beforeend', svg); } else { - const el = document.createElement("html"); + const el = document.createElement('html'); el.innerHTML = result; - + // remove sodipodi and inkscape attributes - el.querySelectorAll("*").forEach(el => { + el.querySelectorAll('*').forEach((el) => { const attributes = el.getAttributeNames(); - attributes.forEach(attr => { - if (attr.includes("inkscape") || attr.includes("sodipodi")) el.removeAttribute(attr); + attributes.forEach((attr) => { + if (attr.includes('inkscape') || attr.includes('sodipodi')) el.removeAttribute(attr); }); }); - const svg = el.querySelector("svg"); - if (!svg) { - tip("The file should be prepated for load to FMG. Please use Armoria or other relevant tools", false, "error"); - return; - } + const svg = el.querySelector('svg'); + if (!svg) return tip('The file should be prepated for load to FMG. Please use Armoria or other relevant tools', false, 'error'); const newEmblem = defs.appendChild(svg); newEmblem.id = id; - newEmblem.setAttribute("width", 200); - newEmblem.setAttribute("height", 200); + newEmblem.setAttribute('width', 200); + newEmblem.setAttribute('height', 200); } if (coa) coa.remove(); // remove old emblem - el.coa = "custom"; + el.coa = 'custom'; emblemShapeSelector.disabled = true; }; - if (type === "image") reader.readAsDataURL(file); else reader.readAsText(file); + if (type === 'image') reader.readAsDataURL(file); + else reader.readAsText(file); } function toggleDownload() { - document.getElementById("emblemUploadControl").classList.add("hidden"); - const buttons = document.getElementById("emblemDownloadControl"); - buttons.classList.toggle("hidden"); + document.getElementById('emblemUploadControl').classList.add('hidden'); + const buttons = document.getElementById('emblemDownloadControl'); + buttons.classList.toggle('hidden'); } async function download(format) { const coa = document.getElementById(id); const size = +emblemsDownloadSize.value; const url = await getURL(coa, size); - const link = document.createElement("a"); - link.download = getFileName(`Emblem ${el.fullName || el.name}`) + "." + format; + const link = document.createElement('a'); + link.download = getFileName(`Emblem ${el.fullName || el.name}`) + '.' + format; - if (format === "svg") downloadSVG(url, link); else downloadRaster(format, url, link, size); - document.getElementById("emblemDownloadControl").classList.add("hidden"); + if (format === 'svg') downloadSVG(url, link); + else downloadRaster(format, url, link, size); + document.getElementById('emblemDownloadControl').classList.add('hidden'); } function downloadSVG(url, link) { @@ -292,24 +293,24 @@ function editEmblem(type, id, el) { } function downloadRaster(format, url, link, size) { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); canvas.width = size; canvas.height = size; const img = new Image(); img.src = url; - img.onload = function() { - if (format === "jpeg") { - ctx.fillStyle = "#fff"; + img.onload = function () { + if (format === 'jpeg') { + ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, canvas.width, canvas.height); } ctx.drawImage(img, 0, 0, canvas.width, canvas.height); - const dataURL = canvas.toDataURL("image/" + format, .92); + const dataURL = canvas.toDataURL('image/' + format, 0.92); link.href = dataURL; link.click(); window.setTimeout(() => window.URL.revokeObjectURL(dataURL), 6000); - } + }; } async function getURL(svg, size) { @@ -322,61 +323,85 @@ function editEmblem(type, id, el) { function getSVG(svg, size) { const clone = svg.cloneNode(true); - clone.setAttribute("width", size); - clone.setAttribute("height", size); - return (new XMLSerializer()).serializeToString(clone); + clone.setAttribute('width', size); + clone.setAttribute('height', size); + return new XMLSerializer().serializeToString(clone); } async function downloadGallery() { - const name = getFileName("Emblems Gallery"); - const validStates = pack.states.filter(s => s.i && !s.removed && s.coa); - const validProvinces = pack.provinces.filter(p => p.i && !p.removed && p.coa); - const validBurgs = pack.burgs.filter(b => b.i && !b.removed && b.coa); + const name = getFileName('Emblems Gallery'); + const validStates = pack.states.filter((s) => s.i && !s.removed && s.coa); + const validProvinces = pack.provinces.filter((p) => p.i && !p.removed && p.coa); + const validBurgs = pack.burgs.filter((b) => b.i && !b.removed && b.coa); await renderAllEmblems(validStates, validProvinces, validBurgs); runDownload(); function runDownload() { const back = `Go Back`; - const stateSection = `States` + validStates.map(state => { - const el = document.getElementById("stateCOA"+state.i); - return `${state.fullName}${getSVG(el, 200)}`; - }).join("") + ``; + const stateSection = + `States` + + validStates + .map((state) => { + const el = document.getElementById('stateCOA' + state.i); + return `${state.fullName}${getSVG(el, 200)}`; + }) + .join('') + + ``; - const provinceSections = validStates.map(state => { - const stateProvinces = validProvinces.filter(p => p.state === state.i); - const figures = stateProvinces.map(province => { - const el = document.getElementById("provinceCOA"+province.i); - return `${province.fullName}${getSVG(el, 200)}`; - }).join(""); - return stateProvinces.length ? `${back}${state.fullName} provinces${figures}` : ""; - }).join(""); + const provinceSections = validStates + .map((state) => { + const stateProvinces = validProvinces.filter((p) => p.state === state.i); + const figures = stateProvinces + .map((province) => { + const el = document.getElementById('provinceCOA' + province.i); + return `${province.fullName}${getSVG(el, 200)}`; + }) + .join(''); + return stateProvinces.length ? `${back}${state.fullName} provinces${figures}` : ''; + }) + .join(''); - const burgSections = validStates.map(state => { - const stateBurgs = validBurgs.filter(b => b.state === state.i); - let stateBurgSections = validProvinces.filter(p => p.state === state.i).map(province => { - const provinceBurgs = stateBurgs.filter(b => pack.cells.province[b.cell] === province.i); - const provinceBurgFigures = provinceBurgs.map(burg => { - const el = document.getElementById("burgCOA"+burg.i); - return `${burg.name}${getSVG(el, 200)}`; - }).join(""); - return provinceBurgs.length ? `${back}${province.fullName} burgs${provinceBurgFigures}` : ""; - }).join(""); + const burgSections = validStates + .map((state) => { + const stateBurgs = validBurgs.filter((b) => b.state === state.i); + let stateBurgSections = validProvinces + .filter((p) => p.state === state.i) + .map((province) => { + const provinceBurgs = stateBurgs.filter((b) => pack.cells.province[b.cell] === province.i); + const provinceBurgFigures = provinceBurgs + .map((burg) => { + const el = document.getElementById('burgCOA' + burg.i); + return `${burg.name}${getSVG(el, 200)}`; + }) + .join(''); + return provinceBurgs.length ? `${back}${province.fullName} burgs${provinceBurgFigures}` : ''; + }) + .join(''); - const stateBurgOutOfProvinces = stateBurgs.filter(b => !pack.cells.province[b.cell]); - const stateBurgOutOfProvincesFigures = stateBurgOutOfProvinces.map(burg => { - const el = document.getElementById("burgCOA"+burg.i); - return `${burg.name}${getSVG(el, 200)}`; - }).join(""); - if (stateBurgOutOfProvincesFigures) stateBurgSections += `${state.fullName} burgs under direct control${stateBurgOutOfProvincesFigures}`; - return stateBurgSections; - }).join(""); + const stateBurgOutOfProvinces = stateBurgs.filter((b) => !pack.cells.province[b.cell]); + const stateBurgOutOfProvincesFigures = stateBurgOutOfProvinces + .map((burg) => { + const el = document.getElementById('burgCOA' + burg.i); + return `${burg.name}${getSVG(el, 200)}`; + }) + .join(''); + if (stateBurgOutOfProvincesFigures) stateBurgSections += `${state.fullName} burgs under direct control${stateBurgOutOfProvincesFigures}`; + return stateBurgSections; + }) + .join(''); - const neutralBurgs = validBurgs.filter(b => !b.state); - const neutralsSection = neutralBurgs.length ? "Independent burgs" + neutralBurgs.map(burg => { - const el = document.getElementById("burgCOA"+burg.i); - return `${burg.name}${getSVG(el, 200)}`; - }).join("") + "" : ""; + const neutralBurgs = validBurgs.filter((b) => !b.state); + const neutralsSection = neutralBurgs.length + ? 'Independent burgs' + + neutralBurgs + .map((burg) => { + const el = document.getElementById('burgCOA' + burg.i); + return `${burg.name}${getSVG(el, 200)}`; + }) + .join('') + + '' + : ''; const FMG = `Azgaar's Fantasy Map Generator`; const license = `the license`; @@ -402,32 +427,33 @@ function editEmblem(type, id, el) { ${neutralsSection} Generated by ${FMG}. The tool is free, but images may be copyrighted, see ${license}