'use strict'; function editLabel() { if (customization) return; closeDialogs(); if (!layerIsOn('toggleLabels')) toggleLabels(); const tspan = d3.event.target; const textPath = tspan.parentNode; const text = textPath.parentNode; elSelected = d3.select(text).call(d3.drag().on('start', dragLabel)).classed('draggable', true); viewbox.on('touchmove mousemove', showEditorTips); $('#labelEditor').dialog({ title: 'Edit Label', resizable: false, width: fitContent(), position: {my: 'center top+10', at: 'bottom', of: text, collision: 'fit'}, close: closeLabelEditor }); drawControlPointsAndLine(); selectLabelGroup(text); updateValues(textPath); if (modules.editLabel) return; modules.editLabel = true; // add listeners document.getElementById('labelGroupShow').addEventListener('click', showGroupSection); document.getElementById('labelGroupHide').addEventListener('click', hideGroupSection); document.getElementById('labelGroupSelect').addEventListener('click', changeGroup); document.getElementById('labelGroupInput').addEventListener('change', createNewGroup); document.getElementById('labelGroupNew').addEventListener('click', toggleNewGroupInput); document.getElementById('labelGroupRemove').addEventListener('click', removeLabelsGroup); document.getElementById('labelTextShow').addEventListener('click', showTextSection); document.getElementById('labelTextHide').addEventListener('click', hideTextSection); document.getElementById('labelText').addEventListener('input', changeText); document.getElementById('labelTextRandom').addEventListener('click', generateRandomName); document.getElementById('labelEditStyle').addEventListener('click', editGroupStyle); document.getElementById('labelSizeShow').addEventListener('click', showSizeSection); document.getElementById('labelSizeHide').addEventListener('click', hideSizeSection); document.getElementById('labelStartOffset').addEventListener('input', changeStartOffset); document.getElementById('labelRelativeSize').addEventListener('input', changeRelativeSize); document.getElementById('labelAlign').addEventListener('click', editLabelAlign); document.getElementById('labelLegend').addEventListener('click', editLabelLegend); document.getElementById('labelRemoveSingle').addEventListener('click', removeLabel); function showEditorTips() { showMainTip(); if (d3.event.target.parentNode.parentNode.id === elSelected.attr('id')) tip('Drag to shift the label'); else if (d3.event.target.parentNode.id === 'controlPoints') { if (d3.event.target.tagName === 'circle') tip('Drag to move, click to delete the control point'); if (d3.event.target.tagName === 'path') tip('Click to add a control point'); } } function selectLabelGroup(text) { const group = text.parentNode.id; const select = document.getElementById('labelGroupSelect'); select.options.length = 0; // remove all options labels.selectAll(':scope > g').each(function () { if (this.id === 'burgLabels') return; select.options.add(new Option(this.id, this.id, false, this.id === group)); }); } function updateValues(textPath) { document.getElementById('labelText').value = [...textPath.querySelectorAll('tspan')].map((tspan) => tspan.textContent).join('|'); document.getElementById('labelStartOffset').value = parseFloat(textPath.getAttribute('startOffset')); document.getElementById('labelRelativeSize').value = parseFloat(textPath.getAttribute('font-size')); } function drawControlPointsAndLine() { debug.select('#controlPoints').remove(); debug.append('g').attr('id', 'controlPoints').attr('transform', elSelected.attr('transform')); const path = document.getElementById('textPath_' + elSelected.attr('id')); debug.select('#controlPoints').append('path').attr('d', path.getAttribute('d')).on('click', addInterimControlPoint); const l = path.getTotalLength(); if (!l) return; const increment = l / Math.max(Math.ceil(l / 200), 2); for (let i = 0; i <= l; i += increment) { addControlPoint(path.getPointAtLength(i)); } } function addControlPoint(point) { debug .select('#controlPoints') .append('circle') .attr('cx', point.x) .attr('cy', point.y) .attr('r', 2.5) .attr('stroke-width', 0.8) .call(d3.drag().on('drag', dragControlPoint)) .on('click', clickControlPoint); } function dragControlPoint() { this.setAttribute('cx', d3.event.x); this.setAttribute('cy', d3.event.y); redrawLabelPath(); } function redrawLabelPath() { const path = document.getElementById('textPath_' + elSelected.attr('id')); lineGen.curve(d3.curveBundle.beta(1)); const points = []; debug .select('#controlPoints') .selectAll('circle') .each(function () { points.push([this.getAttribute('cx'), this.getAttribute('cy')]); }); const d = round(lineGen(points)); path.setAttribute('d', d); debug.select('#controlPoints > path').attr('d', d); } function clickControlPoint() { this.remove(); redrawLabelPath(); } function addInterimControlPoint() { const point = d3.mouse(this); const dists = []; debug .select('#controlPoints') .selectAll('circle') .each(function () { const x = +this.getAttribute('cx'); const y = +this.getAttribute('cy'); dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2); }); let index = dists.length; if (dists.length > 1) { const sorted = dists.slice(0).sort((a, b) => a - b); const closest = dists.indexOf(sorted[0]); const next = dists.indexOf(sorted[1]); if (closest <= next) index = closest + 1; else index = next + 1; } const before = ':nth-child(' + (index + 2) + ')'; debug .select('#controlPoints') .insert('circle', before) .attr('cx', point[0]) .attr('cy', point[1]) .attr('r', 2.5) .attr('stroke-width', 0.8) .call(d3.drag().on('drag', dragControlPoint)) .on('click', clickControlPoint); redrawLabelPath(); } function dragLabel() { const tr = parseTransform(elSelected.attr('transform')); const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y; d3.event.on('drag', function () { const x = d3.event.x, y = d3.event.y; const transform = `translate(${dx + x},${dy + y})`; elSelected.attr('transform', transform); debug.select('#controlPoints').attr('transform', transform); }); } function showGroupSection() { document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'none')); document.getElementById('labelGroupSection').style.display = 'inline-block'; } function hideGroupSection() { document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'inline-block')); document.getElementById('labelGroupSection').style.display = 'none'; document.getElementById('labelGroupInput').style.display = 'none'; document.getElementById('labelGroupInput').value = ''; document.getElementById('labelGroupSelect').style.display = 'inline-block'; } function changeGroup() { document.getElementById(this.value).appendChild(elSelected.node()); } function toggleNewGroupInput() { if (labelGroupInput.style.display === 'none') { labelGroupInput.style.display = 'inline-block'; labelGroupInput.focus(); labelGroupSelect.style.display = 'none'; } else { labelGroupInput.style.display = 'none'; labelGroupSelect.style.display = 'inline-block'; } } function createNewGroup() { if (!this.value) { tip('Please provide a valid group name'); return; } const group = this.value .toLowerCase() .replace(/ /g, '_') .replace(/[^\w\s]/gi, ''); if (document.getElementById(group)) { tip('Element with this id already exists. Please provide a unique name', false, 'error'); return; } if (Number.isFinite(+group.charAt(0))) { tip('Group name should start with a letter', false, 'error'); return; } // just rename if only 1 element left const oldGroup = elSelected.node().parentNode; if (oldGroup !== 'states' && oldGroup !== 'addedLabels' && oldGroup.childElementCount === 1) { document.getElementById('labelGroupSelect').selectedOptions[0].remove(); document.getElementById('labelGroupSelect').options.add(new Option(group, group, false, true)); oldGroup.id = group; toggleNewGroupInput(); document.getElementById('labelGroupInput').value = ''; return; } const newGroup = elSelected.node().parentNode.cloneNode(false); document.getElementById('labels').appendChild(newGroup); newGroup.id = group; document.getElementById('labelGroupSelect').options.add(new Option(group, group, false, true)); document.getElementById(group).appendChild(elSelected.node()); toggleNewGroupInput(); document.getElementById('labelGroupInput').value = ''; } function removeLabelsGroup() { const group = elSelected.node().parentNode.id; const basic = group === 'states' || group === 'addedLabels'; const count = elSelected.node().parentNode.childElementCount; const message = `Are you sure you want to remove ${basic ? 'all elements in the group' : 'the entire label group'}?

Labels to be removed: ${count}`; const onConfirm = () => { $('#labelEditor').dialog('close'); hideGroupSection(); labels .select('#' + group) .selectAll('text') .each(function () { document.getElementById('textPath_' + this.id).remove(); this.remove(); }); if (!basic) labels.select('#' + group).remove(); }; confirmationDialog({title: 'Remove label group', message, confirm: 'Remove', onConfirm}); } function showTextSection() { document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'none')); document.getElementById('labelTextSection').style.display = 'inline-block'; } function hideTextSection() { document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'inline-block')); document.getElementById('labelTextSection').style.display = 'none'; } function changeText() { const input = document.getElementById('labelText').value; const el = elSelected.select('textPath').node(); const example = d3.select(elSelected.node().parentNode).append('text').attr('x', 0).attr('x', 0).attr('font-size', el.getAttribute('font-size')).node(); const lines = input.split('|'); const top = (lines.length - 1) / -2; // y offset const inner = lines .map((l, d) => { example.innerHTML = l; const left = example.getBBox().width / -2; // x offset return `${l}`; }) .join(''); el.innerHTML = inner; example.remove(); if (elSelected.attr('id').slice(0, 10) === 'stateLabel') tip('Use States Editor to change an actual state name, not just a label', false, 'warning'); } function generateRandomName() { let name = ''; if (elSelected.attr('id').slice(0, 10) === 'stateLabel') { const id = +elSelected.attr('id').slice(10); const culture = pack.states[id].culture; name = Names.getState(Names.getCulture(culture, 4, 7, ''), culture); } else { const box = elSelected.node().getBBox(); const cell = findCell((box.x + box.width) / 2, (box.y + box.height) / 2); const culture = pack.cells.culture[cell]; name = Names.getCulture(culture); } document.getElementById('labelText').value = name; changeText(); } function editGroupStyle() { const g = elSelected.node().parentNode.id; editStyle('labels', g); } function showSizeSection() { document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'none')); document.getElementById('labelSizeSection').style.display = 'inline-block'; } function hideSizeSection() { document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'inline-block')); document.getElementById('labelSizeSection').style.display = 'none'; } function changeStartOffset() { elSelected.select('textPath').attr('startOffset', this.value + '%'); tip('Label offset: ' + this.value + '%'); } function changeRelativeSize() { elSelected.select('textPath').attr('font-size', this.value + '%'); tip('Label relative size: ' + this.value + '%'); changeText(); } function editLabelAlign() { const bbox = elSelected.node().getBBox(); const c = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2]; const path = defs.select('#textPath_' + elSelected.attr('id')); path.attr('d', `M${c[0] - bbox.width},${c[1]}h${bbox.width * 2}`); drawControlPointsAndLine(); } function editLabelLegend() { const id = elSelected.attr('id'); const name = elSelected.text(); editNotes(id, name); } function removeLabel() { const message = 'Are you sure you want to remove the label?
This action cannot be reverted'; const onConfirm = () => { defs.select('#textPath_' + elSelected.attr('id')).remove(); elSelected.remove(); $('#labelEditor').dialog('close'); }; confirmationDialog({title: 'Remove label', message, confirm: 'Remove', onConfirm}); } function closeLabelEditor() { debug.select('#controlPoints').remove(); unselect(); } }