Merge branch 'master' of https://github.com/Azgaar/Fantasy-Map-Generator into dev-economics

This commit is contained in:
Azgaar 2021-08-05 00:09:16 +03:00
commit 1180a3c67b
41 changed files with 5185 additions and 3469 deletions

File diff suppressed because one or more lines are too long

View file

@ -14,14 +14,14 @@ function restoreDefaultEvents() {
function clicked() {
const el = d3.event.target;
if (!el || !el.parentElement || !el.parentElement.parentElement) return;
const parent = el.parentElement,
grand = parent.parentElement,
great = grand.parentElement;
const parent = el.parentElement;
const grand = parent.parentElement;
const great = grand.parentElement;
const p = d3.mouse(this);
const i = findCell(p[0], p[1]);
if (grand.id === 'emblems') editEmblem();
else if (parent.id === 'rivers') editRiver();
else if (parent.id === 'rivers') editRiver(el.id);
else if (grand.id === 'routes') editRoute();
else if (el.tagName === 'tspan' && grand.parentNode.parentNode.id === 'labels') editLabel();
else if (grand.id === 'burgLabels') editBurg();
@ -118,33 +118,6 @@ function applySorting(headers) {
.forEach((line) => list.appendChild(line));
}
function confirmationDialog(options) {
const {
title = 'Confirm action',
message = 'Are you sure you want to continue? <br>The action cannot be reverted',
cancel = 'Cancel',
confirm = 'Continue',
onCancel = () => {},
onConfirm = () => {}
} = options;
alertMessage.innerHTML = message;
$('#alert').dialog({
resizable: false,
title,
buttons: {
[confirm]: function () {
onConfirm();
$(this).dialog('close');
},
[cancel]: function () {
onCancel();
$(this).dialog('close');
}
}
});
}
function addBurg(point) {
const cells = pack.cells;
const x = rn(point[0], 2),
@ -405,15 +378,14 @@ function clearLegend() {
}
// draw color (fill) picker
function createPicker(hatching) {
const COLORS_IN_ROW = 14;
function createPicker() {
const pos = () => tip('Drag to change the picker position');
const cl = () => tip('Click to close the picker');
const closePicker = () => container.remove();
const closePicker = () => contaiter.style('display', 'none');
const container = d3.select('body').append('svg').attr('id', 'pickerContainer').attr('width', '100%').attr('height', '100%');
container.append('rect').attr('width', '100%').attr('height', '100%').attr('opacity', 0).on('mousemove', cl).on('click', closePicker);
const picker = container
const contaiter = d3.select('body').append('svg').attr('id', 'pickerContainer').attr('width', '100%').attr('height', '100%');
contaiter.append('rect').attr('x', 0).attr('y', 0).attr('width', '100%').attr('height', '100%').attr('opacity', 0.2).on('mousemove', cl).on('click', closePicker);
const picker = contaiter
.append('g')
.attr('id', 'picker')
.call(
@ -469,7 +441,11 @@ function createPicker(hatching) {
spaces.selectAll('input').on('change', changePickerSpace);
const colors = picker.append('g').attr('id', 'pickerColors').attr('stroke', '#333333');
const clr = d3.range(COLORS_IN_ROW).map((i) => d3.hsl((i / COLORS_IN_ROW) * 360, 0.7, 0.7).hex());
const hatches = picker.append('g').attr('id', 'pickerHatches').attr('stroke', '#333333');
const hatching = d3.selectAll('g#hatching > pattern');
const number = hatching.size();
const clr = d3.range(number).map((i) => d3.hsl((i / number) * 360, 0.7, 0.7).hex());
clr.forEach(function (d, i) {
colors
.append('rect')
@ -481,28 +457,26 @@ function createPicker(hatching) {
.attr('width', 16)
.attr('height', 16);
});
hatching.each(function (d, i) {
hatches
.append('rect')
.attr('id', 'picker_' + this.id)
.attr('fill', 'url(#' + this.id + ')')
.attr('x', i * 22 + 4)
.attr('y', 61)
.attr('width', 16)
.attr('height', 16);
});
colors
.selectAll('rect')
.on('click', pickerFillClicked)
.on('mousemove', () => tip('Click to fill with the color'));
if (hatching) {
const hatches = picker.append('g').attr('id', 'pickerHatches').attr('stroke', '#333333');
d3.selectAll('g#hatching > pattern').each(function (d, i) {
hatches
.append('rect')
.attr('id', 'picker_' + this.id)
.attr('fill', 'url(#' + this.id + ')')
.attr('x', i * 22 + 4)
.attr('y', 61)
.attr('width', 16)
.attr('height', 16);
});
hatches
.selectAll('rect')
.on('click', pickerFillClicked)
.on('mousemove', () => tip('Click to fill with the hatching'));
}
hatches
.selectAll('rect')
.on('click', pickerFillClicked)
.on('mousemove', () => tip('Click to fill with the hatching'));
// append box
const bbox = picker.node().getBBox();
@ -973,6 +947,7 @@ function selectIcon(initial, callback) {
// Calls the refresh for all currently open editors
function refreshAllEditors() {
TIME && console.time('refreshAllEditors');
if (document.getElementById('culturesEditorRefresh').offsetParent) culturesEditorRefresh.click();
if (document.getElementById('biomesEditorRefresh').offsetParent) biomesEditorRefresh.click();
if (document.getElementById('diplomacyEditorRefresh').offsetParent) diplomacyEditorRefresh.click();
@ -980,5 +955,5 @@ function refreshAllEditors() {
if (document.getElementById('religionsEditorRefresh').offsetParent) religionsEditorRefresh.click();
if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click();
if (document.getElementById('zonesEditorRefresh').offsetParent) zonesEditorRefresh.click();
if (document.getElementById('resourcesEditorRefresh').offsetParent) resourcesEditorRefresh.click();
TIME && console.timeEnd('refreshAllEditors');
}

View file

@ -22,7 +22,7 @@ document.getElementById('exitCustomization').addEventListener('mousemove', showD
/**
* @param {string} tip Tooltip text
* @param {boolean} main Show above other tooltips
* @param {string} type Message type (color): error, warn, success
* @param {string} type Message type (color): error / warn / success
* @param {number} time Timeout to auto hide, ms
*/
function tip(tip = 'Tip is undefined', main, type, time) {
@ -96,10 +96,7 @@ function showMapTooltip(point, e, i, g) {
const land = pack.cells.h[i] >= 20;
// specific elements
if (group === 'armies') {
tip(e.target.parentNode.dataset.name + '. Click to edit');
return;
}
if (group === 'armies') return tip(e.target.parentNode.dataset.name + '. Click to edit');
if (group === 'emblems' && e.target.tagName === 'use') {
const parent = e.target.parentNode;
@ -130,14 +127,11 @@ function showMapTooltip(point, e, i, g) {
if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000);
return;
}
if (group === 'routes') {
tip('Click to edit the Route');
return;
}
if (group === 'terrain') {
tip('Click to edit the Relief Icon');
return;
}
if (group === 'routes') return tip('Click to edit the Route');
if (group === 'terrain') return tip('Click to edit the Relief Icon');
if (subgroup === 'burgLabels' || subgroup === 'burgIcons') {
const burg = +path[path.length - 10].dataset.id;
const b = pack.burgs[burg];
@ -146,50 +140,25 @@ function showMapTooltip(point, e, i, g) {
if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
return;
}
if (group === 'labels') {
tip('Click to edit the Label');
return;
}
if (group === 'markers') {
tip('Click to edit the Marker');
return;
}
if (group === 'labels') return tip('Click to edit the Label');
if (group === 'markers') return tip('Click to edit the Marker');
if (group === 'ruler') {
const tag = e.target.tagName;
const className = e.target.getAttribute('class');
if (tag === 'circle' && className === 'edge') {
tip('Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point');
return;
}
if (tag === 'circle' && className === 'control') {
tip('Drag to adjust. Hold Shifta and drag to keep axial direction. Click to remove the point');
return;
}
if (tag === 'circle') {
tip('Drag to adjust the measurer');
return;
}
if (tag === 'polyline') {
tip('Click on drag to add a control point');
return;
}
if (tag === 'path') {
tip('Drag to move the measurer');
return;
}
if (tag === 'text') {
tip('Drag to move, click to remove the measurer');
return;
}
}
if (subgroup === 'burgIcons') {
tip('Click to edit the Burg');
return;
}
if (subgroup === 'burgLabels') {
tip('Click to edit the Burg');
return;
if (tag === 'circle' && className === 'edge') return tip('Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point');
if (tag === 'circle' && className === 'control') return tip('Drag to adjust. Hold Shift and drag to keep axial direction. Click to remove the point');
if (tag === 'circle') return tip('Drag to adjust the measurer');
if (tag === 'polyline') return tip('Click on drag to add a control point');
if (tag === 'path') return tip('Drag to move the measurer');
if (tag === 'text') return tip('Drag to move, click to remove the measurer');
}
if (subgroup === 'burgIcons') return tip('Click to edit the Burg');
if (subgroup === 'burgLabels') return tip('Click to edit the Burg');
if (group === 'lakes' && !land) {
const lakeId = +e.target.dataset.f;
const name = pack.features[lakeId]?.name;
@ -197,20 +166,16 @@ function showMapTooltip(point, e, i, g) {
tip(`${fullName} lake. Click to edit`);
return;
}
if (group === 'coastline') {
tip('Click to edit the coastline');
return;
}
if (group === 'coastline') return tip('Click to edit the coastline');
if (group === 'zones') {
const zone = path[path.length - 8];
tip(zone.dataset.description);
if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000);
return;
}
if (group === 'ice') {
tip('Click to edit the Ice');
return;
}
if (group === 'ice') return tip('Click to edit the Ice');
// covering elements
if (layerIsOn('togglePrec') && land) tip('Annual Precipitation: ' + getFriendlyPrecipitation(i));

View file

@ -1,4 +1,3 @@
// heightmap-editor module. To be added to window as for now
'use strict';
function editHeightmap() {
@ -134,15 +133,8 @@ function editHeightmap() {
// Exit customization mode
function finalizeHeightmap() {
if (viewbox.select('#heights').selectAll('*').size() < 200) {
tip('Insufficient land area! There should be at least 200 land cells to finalize the heightmap', null, 'error');
return;
}
if (document.getElementById('imageConverter').offsetParent) {
tip('Please exit the Image Conversion mode first', null, 'error');
return;
}
if (viewbox.select('#heights').selectAll('*').size() < 200) return tip('Insufficient land area! There should be at least 200 land cells to finalize the heightmap', null, 'error');
if (document.getElementById('imageConverter').offsetParent) return tip('Please exit the Image Conversion mode first', null, 'error');
delete window.edits; // remove global variable
redo.disabled = templateRedo.disabled = true;
@ -207,6 +199,7 @@ function editHeightmap() {
}
}
drawRivers();
Lakes.defineGroup();
defineBiomes();
@ -586,6 +579,7 @@ function editHeightmap() {
document.getElementById('brushesSliders').style.display = 'none';
}
const dragBrushThrottled = throttle(dragBrush, 100);
function toggleBrushMode(e) {
if (e.target.classList.contains('pressed')) {
exitBrushMode();
@ -594,7 +588,7 @@ function editHeightmap() {
exitBrushMode();
document.getElementById('brushesSliders').style.display = 'block';
e.target.classList.add('pressed');
viewbox.style('cursor', 'crosshair').call(d3.drag().on('start', dragBrush));
viewbox.style('cursor', 'crosshair').call(d3.drag().on('start', dragBrushThrottled));
}
function dragBrush() {
@ -842,118 +836,15 @@ function editHeightmap() {
body.setAttribute('data-changed', 0);
body.innerHTML = '';
if (template === 'templateVolcano') {
addStep('Hill', '1', '90-100', '44-56', '40-60');
addStep('Multiply', 0.8, '50-100');
addStep('Range', '1.5', '30-55', '45-55', '40-60');
addStep('Smooth', 2);
addStep('Hill', '1.5', '25-35', '25-30', '20-75');
addStep('Hill', '1', '25-35', '75-80', '25-75');
addStep('Hill', '0.5', '20-25', '10-15', '20-25');
} else if (template === 'templateHighIsland') {
addStep('Hill', '1', '90-100', '65-75', '47-53');
addStep('Add', 5, 'all');
addStep('Hill', '6', '20-23', '25-55', '45-55');
addStep('Range', '1', '40-50', '45-55', '45-55');
addStep('Smooth', 2);
addStep('Trough', '2-3', '20-30', '20-30', '20-30');
addStep('Trough', '2-3', '20-30', '60-80', '70-80');
addStep('Hill', '1', '10-15', '60-60', '50-50');
addStep('Hill', '1.5', '13-16', '15-20', '20-75');
addStep('Multiply', 0.8, '20-100');
addStep('Range', '1.5', '30-40', '15-85', '30-40');
addStep('Range', '1.5', '30-40', '15-85', '60-70');
addStep('Pit', '2-3', '10-15', '15-85', '20-80');
} else if (template === 'templateLowIsland') {
addStep('Hill', '1', '90-99', '60-80', '45-55');
addStep('Hill', '4-5', '25-35', '20-65', '40-60');
addStep('Range', '1', '40-50', '45-55', '45-55');
addStep('Smooth', 3);
addStep('Trough', '1.5', '20-30', '15-85', '20-30');
addStep('Trough', '1.5', '20-30', '15-85', '70-80');
addStep('Hill', '1.5', '10-15', '5-15', '20-80');
addStep('Hill', '1', '10-15', '85-95', '70-80');
addStep('Pit', '3-5', '10-15', '15-85', '20-80');
addStep('Multiply', 0.4, '20-100');
} else if (template === 'templateContinents') {
addStep('Hill', '1', '80-85', '75-80', '40-60');
addStep('Hill', '1', '80-85', '20-25', '40-60');
addStep('Multiply', 0.22, '20-100');
addStep('Hill', '5-6', '15-20', '25-75', '20-82');
addStep('Range', '.8', '30-60', '5-15', '20-45');
addStep('Range', '.8', '30-60', '5-15', '55-80');
addStep('Range', '0-3', '30-60', '80-90', '20-80');
addStep('Trough', '3-4', '15-20', '15-85', '20-80');
addStep('Strait', '2', 'vertical');
addStep('Smooth', 2);
addStep('Trough', '1-2', '5-10', '45-55', '45-55');
addStep('Pit', '3-4', '10-15', '15-85', '20-80');
addStep('Hill', '1', '5-10', '40-60', '40-60');
} else if (template === 'templateArchipelago') {
addStep('Add', 11, 'all');
addStep('Range', '2-3', '40-60', '20-80', '20-80');
addStep('Hill', '5', '15-20', '10-90', '30-70');
addStep('Hill', '2', '10-15', '10-30', '20-80');
addStep('Hill', '2', '10-15', '60-90', '20-80');
addStep('Smooth', 3);
addStep('Trough', '10', '20-30', '5-95', '5-95');
addStep('Strait', '2', 'vertical');
addStep('Strait', '2', 'horizontal');
} else if (template === 'templateAtoll') {
addStep('Hill', '1', '75-80', '50-60', '45-55');
addStep('Hill', '1.5', '30-50', '25-75', '30-70');
addStep('Hill', '.5', '30-50', '25-35', '30-70');
addStep('Smooth', 1);
addStep('Multiply', 0.2, '25-100');
addStep('Hill', '.5', '10-20', '50-55', '48-52');
} else if (template === 'templateMediterranean') {
addStep('Range', '3-4', '30-50', '0-100', '0-10');
addStep('Range', '3-4', '30-50', '0-100', '90-100');
addStep('Hill', '5-6', '30-70', '0-100', '0-5');
addStep('Hill', '5-6', '30-70', '0-100', '95-100');
addStep('Smooth', 1);
addStep('Hill', '2-3', '30-70', '0-5', '20-80');
addStep('Hill', '2-3', '30-70', '95-100', '20-80');
addStep('Multiply', 0.8, 'land');
addStep('Trough', '3-5', '40-50', '0-100', '0-10');
addStep('Trough', '3-5', '40-50', '0-100', '90-100');
} else if (template === 'templatePeninsula') {
addStep('Range', '2-3', '20-35', '40-50', '0-15');
addStep('Add', 5, 'all');
addStep('Hill', '1', '90-100', '10-90', '0-5');
addStep('Add', 13, 'all');
addStep('Hill', '3-4', '3-5', '5-95', '80-100');
addStep('Hill', '1-2', '3-5', '5-95', '40-60');
addStep('Trough', '5-6', '10-25', '5-95', '5-95');
addStep('Smooth', 3);
} else if (template === 'templatePangea') {
addStep('Hill', '1-2', '25-40', '15-50', '0-10');
addStep('Hill', '1-2', '5-40', '50-85', '0-10');
addStep('Hill', '1-2', '25-40', '50-85', '90-100');
addStep('Hill', '1-2', '5-40', '15-50', '90-100');
addStep('Hill', '8-12', '20-40', '20-80', '48-52');
addStep('Smooth', 2);
addStep('Multiply', 0.7, 'land');
addStep('Trough', '3-4', '25-35', '5-95', '10-20');
addStep('Trough', '3-4', '25-35', '5-95', '80-90');
addStep('Range', '5-6', '30-40', '10-90', '35-65');
} else if (template === 'templateIsthmus') {
addStep('Hill', '5-10', '15-30', '0-30', '0-20');
addStep('Hill', '5-10', '15-30', '10-50', '20-40');
addStep('Hill', '5-10', '15-30', '30-70', '40-60');
addStep('Hill', '5-10', '15-30', '50-90', '60-80');
addStep('Hill', '5-10', '15-30', '70-100', '80-100');
addStep('Smooth', 2);
addStep('Trough', '4-8', '15-30', '0-30', '0-20');
addStep('Trough', '4-8', '15-30', '10-50', '20-40');
addStep('Trough', '4-8', '15-30', '30-70', '40-60');
addStep('Trough', '4-8', '15-30', '50-90', '60-80');
addStep('Trough', '4-8', '15-30', '70-100', '80-100');
} else if (template === 'templateShattered') {
addStep('Hill', '8', '35-40', '15-85', '30-70');
addStep('Trough', '10-20', '40-50', '5-95', '5-95');
addStep('Range', '5-7', '30-40', '10-90', '20-80');
addStep('Pit', '12-20', '30-40', '15-85', '20-80');
const templateString = HeightmapTemplates[template];
if (!templateString) return;
const steps = templateString.split('\n');
if (!steps.length) return tip(`Heightmap template: no steps defined`, false, 'error');
for (const step of steps) {
const elements = step.trim().split(' ');
addStep(...elements);
}
}
@ -1119,6 +1010,10 @@ function editHeightmap() {
const reader = new FileReader();
const img = new Image();
img.id = 'imageToConvert';
img.style.display = 'none';
document.body.appendChild(img);
img.onload = function () {
const ctx = document.getElementById('canvas').getContext('2d');
ctx.drawImage(img, 0, 0, graphWidth, graphHeight);
@ -1311,10 +1206,7 @@ function editHeightmap() {
}
function applyConversion() {
if (colorsAssigned.childElementCount < 3) {
tip('Please do the assignment first', false, 'error');
return;
}
if (colorsAssigned.childElementCount < 3) return tip('Please do the assignment first', false, 'error');
viewbox
.select('#heights')
@ -1340,6 +1232,9 @@ function editHeightmap() {
const canvas = document.getElementById('canvas');
if (canvas) canvas.remove();
const image = document.getElementById('imageToConvert');
if (image) image.remove();
d3.select('#imageConverter').selectAll('div.color-div').remove();
colorsAssigned.style.display = 'none';
colorsUnassigned.style.display = 'none';

View file

@ -123,9 +123,10 @@ function restoreLayers() {
if (layerIsOn('toggleIce')) drawIce();
if (layerIsOn('toggleEmblems')) drawEmblems();
// states are getting rendered each time, if it's not required than layers should be hidden
if (!layerIsOn('toggleBorders')) $('#borders').fadeOut();
if (!layerIsOn('toggleStates')) regions.style('display', 'none').selectAll('path').remove();
// some layers are rendered each time, remove them if they are not on
if (!layerIsOn('toggleBorders')) borders.selectAll('path').remove();
if (!layerIsOn('toggleStates')) regions.selectAll('path').remove();
if (!layerIsOn('toggleRivers')) rivers.selectAll('*').remove();
}
function toggleHeight(event) {
@ -876,35 +877,80 @@ function toggleStates(event) {
}
}
// draw states
function drawStates() {
TIME && console.time('drawStates');
regions.selectAll('path').remove();
const cells = pack.cells,
vertices = pack.vertices,
states = pack.states,
n = cells.i.length;
const {cells, vertices, features} = pack;
const states = pack.states;
const n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const vArray = new Array(states.length); // store vertices array
const body = new Array(states.length).fill(''); // store path around each state
const gap = new Array(states.length).fill(''); // store path along water for each state to fill the gaps
const body = new Array(states.length).fill(''); // path around each state
const gap = new Array(states.length).fill(''); // path along water for each state to fill the gaps
const halo = new Array(states.length).fill(''); // path around states, but not lakes
const getStringPoint = (v) => vertices.p[v[0]].join(',');
// define inner-state lakes to omit on border render
const innerLakes = features.map((feature) => {
if (feature.type !== 'lake') return false;
if (!feature.shoreline) Lakes.getShoreline(feature);
const states = feature.shoreline.map((i) => cells.state[i]);
return new Set(states).size > 1 ? false : true;
});
for (const i of cells.i) {
if (!cells.state[i] || used[i]) continue;
const s = cells.state[i];
const onborder = cells.c[i].some((n) => cells.state[n] !== s);
const state = cells.state[i];
const onborder = cells.c[i].some((n) => cells.state[n] !== state);
if (!onborder) continue;
const borderWith = cells.c[i].map((c) => cells.state[c]).find((n) => n !== s);
const borderWith = cells.c[i].map((c) => cells.state[c]).find((n) => n !== state);
const vertex = cells.v[i].find((v) => vertices.c[v].some((i) => cells.state[i] === borderWith));
const chain = connectVertices(vertex, s, borderWith);
if (chain.length < 3) continue;
const points = chain.map((v) => vertices.p[v[0]]);
if (!vArray[s]) vArray[s] = [];
vArray[s].push(points);
body[s] += 'M' + points.join('L');
gap[s] += 'M' + vertices.p[chain[0][0]] + chain.reduce((r, v, i, d) => (!i ? r : !v[2] ? r + 'L' + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r + 'M' + vertices.p[v[0]] : r), '');
const chain = connectVertices(vertex, state);
const noInnerLakes = chain.filter((v) => v[1] !== 'innerLake');
if (noInnerLakes.length < 3) continue;
// get path around the state
if (!vArray[state]) vArray[state] = [];
const points = noInnerLakes.map((v) => vertices.p[v[0]]);
vArray[state].push(points);
body[state] += 'M' + points.join('L');
// connect path for halo
let discontinued = true;
halo[state] += noInnerLakes
.map((v) => {
if (v[1] === 'border') {
discontinued = true;
return '';
}
const operation = discontinued ? 'M' : 'L';
discontinued = false;
return `${operation}${getStringPoint(v)}`;
})
.join('');
// connect gaps between state and water into a single path
discontinued = true;
gap[state] += chain
.map((v) => {
if (v[1] === 'land') {
discontinued = true;
return '';
}
const operation = discontinued ? 'M' : 'L';
discontinued = false;
return `${operation}${getStringPoint(v)}`;
})
.join('');
}
// find state visual center
@ -913,13 +959,14 @@ function drawStates() {
states[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
});
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter((d) => d[0]);
const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter((d) => d[0]);
const bodyData = body.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter((d) => d[0]);
const gapData = gap.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter((d) => d[0]);
const haloData = halo.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter((d) => d[0]);
const bodyString = bodyData.map((d) => `<path id="state${d[1]}" d="${d[0]}" fill="${d[2]}" stroke="none"/>`).join('');
const gapString = gapData.map((d) => `<path id="state-gap${d[1]}" d="${d[0]}" fill="none" stroke="${d[2]}"/>`).join('');
const clipString = bodyData.map((d) => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`).join('');
const haloString = bodyData
const haloString = haloData
.map((d) => `<path id="state-border${d[1]}" d="${d[0]}" clip-path="url(#state-clip${d[1]})" stroke="${d3.color(d[2]) ? d3.color(d[2]).darker().hex() : '#666666'}"/>`)
.join('');
@ -928,57 +975,77 @@ function drawStates() {
statesHalo.html(haloString);
// connect vertices to chain
function connectVertices(start, t, state) {
function connectVertices(start, state) {
const chain = []; // vertices chain to form a path
let land = vertices.c[start].some((c) => cells.h[c] >= 20 && cells.state[c] !== t);
function check(i) {
state = cells.state[i];
land = cells.h[i] >= 20;
}
const getType = (c) => {
const borderCell = c.find((i) => cells.b[i]);
if (borderCell) return 'border';
const waterCell = c.find((i) => cells.h[i] < 20);
if (!waterCell) return 'land';
if (innerLakes[cells.f[waterCell]]) return 'innerLake';
return features[cells.f[waterCell]].type;
};
for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
chain.push([current, state, land]); // add current vertex to sequence
const prev = chain.length ? chain[chain.length - 1][0] : -1; // previous vertex in chain
const c = vertices.c[current]; // cells adjacent to vertex
c.filter((c) => cells.state[c] === t).forEach((c) => (used[c] = 1));
const c0 = c[0] >= n || cells.state[c[0]] !== t;
const c1 = c[1] >= n || cells.state[c[1]] !== t;
const c2 = c[2] >= n || cells.state[c[2]] !== t;
chain.push([current, getType(c)]); // add current vertex to sequence
c.filter((c) => cells.state[c] === state).forEach((c) => (used[c] = 1));
const c0 = c[0] >= n || cells.state[c[0]] !== state;
const c1 = c[1] >= n || cells.state[c[1]] !== state;
const c2 = c[2] >= n || cells.state[c[2]] !== state;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) {
current = v[0];
check(c0 ? c[0] : c[1]);
} else if (v[1] !== prev && c1 !== c2) {
current = v[1];
check(c1 ? c[1] : c[2]);
} else if (v[2] !== prev && c0 !== c2) {
current = v[2];
check(c2 ? c[2] : c[0]);
}
if (current === chain[chain.length - 1][0]) {
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === prev) {
ERROR && console.error('Next vertex is not found');
break;
}
}
chain.push([start, state, land]); // add starting vertex to sequence to close the path
if (chain.length) chain.push(chain[0]);
return chain;
}
invokeActiveZooming();
TIME && console.timeEnd('drawStates');
}
function toggleBorders(event) {
if (!layerIsOn('toggleBorders')) {
turnButtonOn('toggleBorders');
drawBorders();
if (event && isCtrlClick(event)) editStyle('borders');
} else {
if (event && isCtrlClick(event)) {
editStyle('borders');
return;
}
turnButtonOff('toggleBorders');
borders.selectAll('path').remove();
}
}
// draw state and province borders
function drawBorders() {
TIME && console.time('drawBorders');
borders.selectAll('path').remove();
const cells = pack.cells,
vertices = pack.vertices,
n = cells.i.length;
const sPath = [],
pPath = [];
const sUsed = new Array(pack.states.length).fill('').map((a) => []);
const pUsed = new Array(pack.provinces.length).fill('').map((a) => []);
const {cells, vertices} = pack;
const n = cells.i.length;
const sPath = [];
const pPath = [];
const sUsed = new Array(pack.states.length).fill('').map((_) => []);
const pUsed = new Array(pack.provinces.length).fill('').map((_) => []);
for (let i = 0; i < cells.i.length; i++) {
if (!cells.state[i]) continue;
@ -1070,21 +1137,6 @@ function drawBorders() {
TIME && console.timeEnd('drawBorders');
}
function toggleBorders(event) {
if (!layerIsOn('toggleBorders')) {
turnButtonOn('toggleBorders');
$('#borders').fadeIn();
if (event && isCtrlClick(event)) editStyle('borders');
} else {
if (event && isCtrlClick(event)) {
editStyle('borders');
return;
}
turnButtonOff('toggleBorders');
$('#borders').fadeOut();
}
}
function toggleProvinces(event) {
if (!layerIsOn('toggleProvinces')) {
turnButtonOn('toggleProvinces');
@ -1396,18 +1448,30 @@ function toggleTexture(event) {
function toggleRivers(event) {
if (!layerIsOn('toggleRivers')) {
turnButtonOn('toggleRivers');
$('#rivers').fadeIn();
drawRivers();
if (event && isCtrlClick(event)) editStyle('rivers');
} else {
if (event && isCtrlClick(event)) {
editStyle('rivers');
return;
}
$('#rivers').fadeOut();
if (event && isCtrlClick(event)) return editStyle('rivers');
rivers.selectAll('*').remove();
turnButtonOff('toggleRivers');
}
}
function drawRivers() {
TIME && console.time('drawRivers');
const {addMeandering, getRiverPath} = Rivers;
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const riverPaths = pack.rivers.map((river) => {
const meanderedPoints = addMeandering(river.cells, river.points);
const widthFactor = river.widthFactor || 1;
const startingWidth = river.sourceWidth || 0;
const path = getRiverPath(meanderedPoints, widthFactor, startingWidth);
return `<path id="river${river.i}" d="${path}"/>`;
});
rivers.html(riverPaths.join(''));
TIME && console.timeEnd('drawRivers');
}
function toggleRoutes(event) {
if (!layerIsOn('toggleRoutes')) {
turnButtonOn('toggleRoutes');
@ -1558,21 +1622,21 @@ function drawEmblems() {
const getStateEmblemsSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 40, 10), 100);
const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
const sizeMod = +document.getElementById('styleEmblemsStateSizeInput').value || 1;
const sizeMod = +document.getElementById('emblemsStateSizeInput').value || 1;
return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
};
const getProvinceEmblemsSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 100, 5), 70);
const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
const sizeMod = +document.getElementById('styleEmblemsProvinceSizeInput').value || 1;
const sizeMod = +document.getElementById('emblemsProvinceSizeInput').value || 1;
return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
};
const getBurgEmblemSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 185, 2), 50);
const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
const sizeMod = +document.getElementById('styleEmblemsBurgSizeInput').value || 1;
const sizeMod = +document.getElementById('emblemsBurgSizeInput').value || 1;
return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
};

View file

@ -98,7 +98,8 @@ function showSupporters() {
PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram,
Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,
Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,
Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 ,Chris Gray`;
Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 ,Chris Gray,Phoenix Boatwright,Mackenzie,
"Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas"`;
const array = supporters
.replace(/(?:\r\n|\r|\n)/g, '')
@ -157,6 +158,7 @@ optionsContent.addEventListener('change', function (event) {
if (id === 'zoomExtentMin' || id === 'zoomExtentMax') changeZoomExtent(value);
else if (id === 'optionsSeed') generateMapWithSeed();
else if (id === 'uiSizeInput' || id === 'uiSizeOutput') changeUIsize(value);
if (id === 'shapeRendering') viewbox.attr('shape-rendering', value);
else if (id === 'yearInput') changeYear();
else if (id === 'eraInput') changeEra();
});
@ -494,6 +496,9 @@ function applyStoredOptions() {
const height = +params.get('height');
if (width) mapWidthInput.value = width;
if (height) mapHeightInput.value = height;
// set shape rendering
viewbox.attr('shape-rendering', shapeRendering.value);
}
// randomize options if randomization is allowed (not locked or options='default')
@ -537,17 +542,18 @@ function randomizeOptions() {
// select heightmap template pseudo-randomly
function randomizeHeightmapTemplate() {
const templates = {
Volcano: 3,
'High Island': 22,
'Low Island': 9,
Continents: 20,
Archipelago: 25,
Mediterranean: 3,
Peninsula: 3,
Pangea: 5,
Isthmus: 2,
Atoll: 1,
Shattered: 7
volcano: 3,
highIsland: 22,
lowIsland: 9,
continents: 19,
archipelago: 23,
mediterranean: 5,
peninsula: 3,
pangea: 5,
isthmus: 2,
atoll: 1,
shattered: 7,
taklamakan: 1
};
document.getElementById('templateInput').value = rw(templates);
}
@ -773,6 +779,12 @@ document
.forEach((el) => el.addEventListener('input', updateTilesOptions));
function updateTilesOptions() {
if (this?.tagName === 'INPUT') {
const {nextElementSibling: next, previousElementSibling: prev} = this;
if (next?.tagName === 'INPUT') next.value = this.value;
if (prev?.tagName === 'INPUT') prev.value = this.value;
}
const tileSize = document.getElementById('tileSize');
const tilesX = +document.getElementById('tileColsOutput').value;
const tilesY = +document.getElementById('tileRowsOutput').value;
@ -908,6 +920,7 @@ function toggle3dOptions() {
document.getElementById('options3dMeshRotationNumber').addEventListener('change', changeRotation);
document.getElementById('options3dGlobeRotationRange').addEventListener('input', changeRotation);
document.getElementById('options3dGlobeRotationNumber').addEventListener('change', changeRotation);
document.getElementById('options3dMeshLabels3d').addEventListener('change', toggleLabels3d);
document.getElementById('options3dMeshSkyMode').addEventListener('change', toggleSkyMode);
document.getElementById('options3dMeshSky').addEventListener('input', changeColors);
document.getElementById('options3dMeshWater').addEventListener('input', changeColors);
@ -924,6 +937,7 @@ function toggle3dOptions() {
options3dSunZ.value = ThreeD.options.sun.z;
options3dMeshRotationRange.value = options3dMeshRotationNumber.value = ThreeD.options.rotateMesh;
options3dGlobeRotationRange.value = options3dGlobeRotationNumber.value = ThreeD.options.rotateGlobe;
options3dMeshLabels3d.value = ThreeD.options.labels3d;
options3dMeshSkyMode.value = ThreeD.options.extendedWater;
options3dColorSection.style.display = ThreeD.options.extendedWater ? 'block' : 'none';
options3dMeshSky.value = ThreeD.options.skyColor;
@ -954,6 +968,10 @@ function toggle3dOptions() {
ThreeD.setRotation(speed);
}
function toggleLabels3d() {
ThreeD.toggleLabels();
}
function toggleSkyMode() {
const hide = ThreeD.options.extendedWater;
options3dColorSection.style.display = hide ? 'none' : 'block';

View file

@ -81,6 +81,7 @@ function editReliefIcon() {
reliefSpacingDiv.style.display = 'none';
reliefIconsSeletionAny.style.display = 'none';
removeCircle();
updateReliefSizeInput();
restoreDefaultEvents();
clearMainTip();
@ -115,10 +116,7 @@ function editReliefIcon() {
function dragToAdd() {
const pressed = reliefIconsDiv.querySelector('svg.pressed');
if (!pressed) {
tip('Please select an icon', false, error);
return;
}
if (!pressed) return tip('Please select an icon', false, error);
const type = pressed.dataset.type;
const r = +reliefRadiusNumber.value;
@ -188,10 +186,7 @@ function editReliefIcon() {
function dragToRemove() {
const pressed = reliefIconsDiv.querySelector('svg.pressed');
if (!pressed) {
tip('Please select an icon', false, error);
return;
}
if (!pressed) return tip('Please select an icon', false, error);
const r = +reliefRadiusNumber.value;
const type = pressed.dataset.type;
@ -256,12 +251,32 @@ function editReliefIcon() {
}
function removeIcon() {
const message = 'Are you sure you want to remove the relief icon? <br>This action cannot be reverted';
const onConfirm = () => {
elSelected.remove();
$('#reliefEditor').dialog('close');
};
confirmationDialog({title: 'Remove relief icon', message, confirm: 'Remove', onConfirm});
let selection = null;
const pressed = reliefTools.querySelector('button.pressed');
if (pressed.id === 'reliefIndividual') {
alertMessage.innerHTML = `Are you sure you want to remove the icon?`;
selection = elSelected;
} else {
const type = reliefIconsDiv.querySelector('svg.pressed')?.dataset.type;
selection = type ? terrain.selectAll("use[href='" + type + "']") : terrain.selectAll('use');
const size = selection.size();
alertMessage.innerHTML = type ? `Are you sure you want to remove all ${type} icons (${size})?` : `Are you sure you want to remove all icons (${size})?`;
}
$('#alert').dialog({
resizable: false,
title: 'Remove relief icons',
buttons: {
Remove: function () {
if (selection) selection.remove();
$(this).dialog('close');
$('#reliefEditor').dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function closeReliefEditor() {

View file

@ -0,0 +1,125 @@
"use strict";
function createRiver() {
if (customization) return;
closeDialogs();
if (!layerIsOn("toggleRivers")) toggleRivers();
document.getElementById("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
if (!layerIsOn("toggleCells")) toggleCells();
tip("Click to add river point, click again to remove", true);
debug.append("g").attr("id", "controlCells");
viewbox.style("cursor", "crosshair").on("click", onCellClick);
createRiver.cells = [];
const body = document.getElementById("riverCreatorBody");
$("#riverCreator").dialog({
title: "Create River",
resizable: false,
position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeRiverCreator
});
if (modules.createRiver) return;
modules.createRiver = true;
// add listeners
document.getElementById("riverCreatorComplete").addEventListener("click", addRiver);
document.getElementById("riverCreatorCancel").addEventListener("click", () => $("#riverCreator").dialog("close"));
body.addEventListener("click", function (ev) {
const el = ev.target;
const cl = el.classList;
const cell = +el.parentNode.dataset.cell;
if (cl.contains("editFlux")) pack.cells.fl[cell] = +el.value;
else if (cl.contains("icon-trash-empty")) removeCell(cell);
});
function onCellClick() {
const cell = findCell(...d3.mouse(this));
if (createRiver.cells.includes(cell)) removeCell(cell);
else addCell(cell);
}
function addCell(cell) {
createRiver.cells.push(cell);
drawCells(createRiver.cells);
const flux = pack.cells.fl[cell];
const line = `<div class="editorLine" data-cell="${cell}">
<span>Cell ${cell}</span>
<span data-tip="Set flux affects river width" style="margin-left: 0.4em">Flux</span>
<input type="number" min=0 value="${flux}" class="editFlux" style="width: 5em"/>
<span data-tip="Remove the cell" class="icon-trash-empty pointer"></span>
</div>`;
body.innerHTML += line;
}
function removeCell(cell) {
createRiver.cells = createRiver.cells.filter(c => c !== cell);
drawCells(createRiver.cells);
body.querySelector(`div[data-cell='${cell}']`)?.remove();
}
function drawCells(cells) {
debug
.select("#controlCells")
.selectAll(`polygon`)
.data(cells)
.join("polygon")
.attr("points", d => getPackPolygon(d))
.attr("class", "current");
}
function addRiver() {
const {rivers, cells} = pack;
const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin} = Rivers;
const riverCells = createRiver.cells;
if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error");
const riverId = rivers.length ? last(rivers).i + 1 : 1;
const parent = cells.r[last(riverCells)] || riverId;
riverCells.forEach(cell => {
if (!cells.r[cell]) cells.r[cell] = riverId;
});
const source = riverCells[0];
const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2];
const sourceWidth = 0.05;
const widthFactor = 1.2;
const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
const name = getName(mouth);
const basin = getBasin(parent);
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type: "River"});
// render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
viewbox
.select("#rivers")
.append("path")
.attr("id", "river" + riverId)
.attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
editRiver(riverId);
}
function closeRiverCreator() {
body.innerHTML = "";
debug.select("#controlCells").remove();
restoreDefaultEvents();
clearMainTip();
const forced = +document.getElementById("toggleCells").dataset.forced;
document.getElementById("toggleCells").dataset.forced = 0;
if (forced && layerIsOn("toggleCells")) toggleCells();
}
}

View file

@ -1,21 +1,31 @@
'use strict';
function editRiver(id) {
if (customization) return;
if (elSelected && d3.event && d3.event.target.id === elSelected.attr('id')) return;
if (elSelected && id === elSelected.attr('id')) return;
closeDialogs('.stable');
if (!layerIsOn('toggleRivers')) toggleRivers();
const node = id ? document.getElementById(id) : d3.event.target;
elSelected = d3.select(node).on('click', addInterimControlPoint);
viewbox.on('touchmove mousemove', showEditorTips);
debug.append('g').attr('id', 'controlPoints').attr('transform', elSelected.attr('transform'));
document.getElementById('toggleCells').dataset.forced = +!layerIsOn('toggleCells');
if (!layerIsOn('toggleCells')) toggleCells();
elSelected = d3.select('#' + id);
tip('Drag control points to change the river course. For major changes please create a new river instead', true);
debug.append('g').attr('id', 'controlCells');
debug.append('g').attr('id', 'controlPoints');
updateRiverData();
drawControlPoints(node);
const river = getRiver();
const {cells, points} = river;
const riverPoints = Rivers.getRiverPoints(cells, points);
drawControlPoints(riverPoints, cells);
drawCells(cells, 'current');
$('#riverEditor').dialog({
title: 'Edit River',
resizable: false,
position: {my: 'center top+80', at: 'top', of: node, collision: 'fit'},
position: {my: 'left top', at: 'left+10 top+10', of: '#map'},
close: closeRiverEditor
});
@ -23,27 +33,19 @@ function editRiver(id) {
modules.editRiver = true;
// add listeners
document.getElementById('riverCreateSelectingCells').addEventListener('click', createRiver);
document.getElementById('riverEditStyle').addEventListener('click', () => editStyle('rivers'));
document.getElementById('riverElevationProfile').addEventListener('click', showElevationProfile);
document.getElementById('riverLegend').addEventListener('click', editRiverLegend);
document.getElementById('riverRemove').addEventListener('click', removeRiver);
document.getElementById('riverName').addEventListener('input', changeName);
document.getElementById('riverType').addEventListener('input', changeType);
document.getElementById('riverNameCulture').addEventListener('click', generateNameCulture);
document.getElementById('riverNameRandom').addEventListener('click', generateNameRandom);
document.getElementById('riverMainstem').addEventListener('change', changeParent);
document.getElementById('riverSourceWidth').addEventListener('input', changeSourceWidth);
document.getElementById('riverWidthFactor').addEventListener('input', changeWidthFactor);
document.getElementById('riverNew').addEventListener('click', toggleRiverCreationMode);
document.getElementById('riverEditStyle').addEventListener('click', () => editStyle('rivers'));
document.getElementById('riverElevationProfile').addEventListener('click', showElevationProfile);
document.getElementById('riverLegend').addEventListener('click', editRiverLegend);
document.getElementById('riverRemove').addEventListener('click', removeRiver);
function showEditorTips() {
showMainTip();
if (d3.event.target.parentNode.id === elSelected.attr('id')) tip('Drag to move, click to add a control point');
else if (d3.event.target.parentNode.id === 'controlPoints') tip('Drag to move, click to delete the control point');
}
function getRiver() {
const riverId = +elSelected.attr('id').slice(5);
const river = pack.rivers.find((r) => r.i === riverId);
@ -67,87 +69,107 @@ function editRiver(id) {
document.getElementById('riverBasin').value = pack.rivers.find((river) => river.i === r.basin).name;
document.getElementById('riverDischarge').value = r.discharge + ' m³/s';
r.length = elSelected.node().getTotalLength() / 2;
const length = rn(r.length * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
document.getElementById('riverLength').value = length;
const width = rn(r.width * distanceScaleInput.value, 3) + ' ' + distanceUnitInput.value;
document.getElementById('riverWidth').value = width;
document.getElementById('riverSourceWidth').value = r.sourceWidth;
document.getElementById('riverWidthFactor').value = r.widthFactor;
updateRiverLength(r);
updateRiverWidth(r);
}
function drawControlPoints(node) {
const length = getRiver().length;
const segments = Math.ceil(length / 4);
const increment = rn((length / segments) * 1e5);
for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i / 1e5);
const p2 = node.getPointAtLength(c / 1e5);
addControlPoint([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2]);
}
function updateRiverLength(river) {
river.length = rn(elSelected.node().getTotalLength() / 2, 2);
const lengthUI = `${rn(river.length * distanceScaleInput.value)} ${distanceUnitInput.value}`;
document.getElementById('riverLength').value = lengthUI;
}
function addControlPoint(point, before = null) {
debug.select('#controlPoints').insert('circle', before).attr('cx', point[0]).attr('cy', point[1]).attr('r', 0.6).call(d3.drag().on('drag', dragControlPoint)).on('click', clickControlPoint);
function updateRiverWidth(river) {
const {addMeandering, getWidth, getOffset} = Rivers;
const {cells, discharge, widthFactor, sourceWidth} = river;
const meanderedPoints = addMeandering(cells);
river.width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
const width = `${rn(river.width * distanceScaleInput.value, 3)} ${distanceUnitInput.value}`;
document.getElementById('riverWidth').value = width;
}
function dragControlPoint() {
this.setAttribute('cx', d3.event.x);
this.setAttribute('cy', d3.event.y);
redrawRiver();
}
function redrawRiver() {
const points = [];
function drawControlPoints(points, cells) {
debug
.select('#controlPoints')
.selectAll('circle')
.each(function () {
points.push([+this.getAttribute('cx'), +this.getAttribute('cy')]);
});
.data(points)
.enter()
.append('circle')
.attr('cx', (d) => d[0])
.attr('cy', (d) => d[1])
.attr('r', 0.6)
.attr('data-cell', (d, i) => cells[i])
.attr('data-i', (d, i) => i)
.call(d3.drag().on('start', dragControlPoint));
}
if (points.length < 2) return;
if (points.length === 2) {
const p0 = points[0],
p1 = points[1];
const angle = Math.atan2(p1[1] - p0[1], p1[0] - p0[0]);
const sin = Math.sin(angle),
cos = Math.cos(angle);
elSelected.attr('d', `M${p0[0]},${p0[1]} L${p1[0]},${p1[1]} l${-sin / 2},${cos / 2} Z`);
return;
}
function drawCells(cells, type) {
debug
.select('#controlCells')
.selectAll(`polygon.${type}`)
.data(cells.filter((i) => pack.cells.i[i]))
.join('polygon')
.attr('points', (d) => getPackPolygon(d))
.attr('class', type);
}
const widthFactor = +document.getElementById('riverWidthFactor').value;
const sourceWidth = +document.getElementById('riverSourceWidth').value;
const [path, length, offset] = Rivers.getPath(points, widthFactor, sourceWidth);
function dragControlPoint() {
const {i, r, fl} = pack.cells;
const river = getRiver();
const initCell = +this.dataset.cell;
const index = +this.dataset.i;
let movedToCell = null;
d3.event.on('drag', function () {
const {x, y} = d3.event;
const currentCell = findCell(x, y);
movedToCell = initCell !== currentCell ? currentCell : null;
this.setAttribute('cx', x);
this.setAttribute('cy', y);
this.__data__ = [rn(x, 1), rn(y, 1)];
redrawRiver();
});
d3.event.on('end', () => {
if (movedToCell) {
this.dataset.cell = movedToCell;
river.cells[index] = movedToCell;
drawCells(river.cells, 'current');
if (!r[movedToCell]) {
// swap river data
r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
}
}
});
}
function redrawRiver() {
const river = getRiver();
river.points = debug.selectAll('#controlPoints > *').data();
const {cells, widthFactor, sourceWidth} = river;
const meanderedPoints = Rivers.addMeandering(cells, river.points);
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
elSelected.attr('d', path);
const r = getRiver();
if (r) {
r.width = rn(offset ** 2, 2);
r.length = length;
updateRiverData();
}
updateRiverLength(river);
if (modules.elevation) showEPForRiver(elSelected.node());
}
function clickControlPoint() {
this.remove();
redrawRiver();
}
function addInterimControlPoint() {
const point = d3.mouse(this);
const controls = document.getElementById('controlPoints').querySelectorAll('circle');
const points = Array.from(controls).map((circle) => [+circle.getAttribute('cx'), +circle.getAttribute('cy')]);
const index = getSegmentId(points, point, 2);
addControlPoint(point, ':nth-child(' + (index + 1) + ')');
redrawRiver();
}
function changeName() {
getRiver().name = this.value;
}
@ -174,12 +196,16 @@ function editRiver(id) {
}
function changeSourceWidth() {
getRiver().sourceWidth = +this.value;
const river = getRiver();
river.sourceWidth = +this.value;
updateRiverWidth(river);
redrawRiver();
}
function changeWidthFactor() {
getRiver().widthFactor = +this.value;
const river = getRiver();
river.widthFactor = +this.value;
updateRiverWidth(river);
redrawRiver();
}
@ -194,81 +220,35 @@ function editRiver(id) {
editNotes(id, river.name + ' ' + river.type);
}
function toggleRiverCreationMode() {
if (document.getElementById('riverNew').classList.contains('pressed')) exitRiverCreationMode();
else {
document.getElementById('riverNew').classList.add('pressed');
tip('Click on map to add control points', true, 'warn');
viewbox.on('click', addPointOnClick).style('cursor', 'crosshair');
elSelected.on('click', null);
}
}
function addPointOnClick() {
if (!elSelected.attr('data-new')) {
debug.select('#controlPoints').selectAll('circle').remove();
const id = getNextId('river');
elSelected = d3.select(elSelected.node().parentNode).append('path').attr('id', id).attr('data-new', 1);
}
// add control point
const point = d3.mouse(this);
addControlPoint([point[0], point[1]]);
redrawRiver();
}
function exitRiverCreationMode() {
riverNew.classList.remove('pressed');
clearMainTip();
viewbox.on('click', clicked).style('cursor', 'default');
elSelected.on('click', addInterimControlPoint);
if (!elSelected.attr('data-new')) return; // no need to create a new river
elSelected.attr('data-new', null);
// add a river
const r = +elSelected.attr('id').slice(5);
const node = elSelected.node(),
length = node.getTotalLength() / 2;
const cells = [];
const segments = Math.ceil(length / 4),
increment = rn((length / segments) * 1e5);
for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) {
const p = node.getPointAtLength(i / 1e5);
const cell = findCell(p.x, p.y);
if (!pack.cells.r[cell]) pack.cells.r[cell] = r;
cells.push(cell);
}
const source = cells[0],
mouth = last(cells);
const name = Rivers.getName(mouth);
const smallLength = pack.rivers.map((r) => r.length || 0).sort((a, b) => a - b)[Math.ceil(pack.rivers.length * 0.15)];
const type = length < smallLength ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : 'River';
const discharge = rn(cells.length * 20 * Math.random());
const widthFactor = +document.getElementById('riverWidthFactor').value;
const sourceWidth = +document.getElementById('riverSourceWidth').value;
pack.rivers.push({i: r, source, mouth, discharge, length, width: sourceWidth, widthFactor, sourceWidth, parent: 0, name, type, basin: r});
}
function removeRiver() {
const message = 'Are you sure you want to remove the river? <br>All tributaries will be auto-removed';
const onConfirm = () => {
const river = +elSelected.attr('id').slice(5);
Rivers.remove(river);
elSelected.remove(); // if river if missed in pack.rivers
$('#riverEditor').dialog('close');
};
confirmationDialog({title: 'Remove river', message, confirm: 'Remove', onConfirm});
alertMessage.innerHTML = 'Are you sure you want to remove the river and all its tributaries';
$('#alert').dialog({
resizable: false,
width: '22em',
title: 'Remove river and tributaries',
buttons: {
Remove: function () {
$(this).dialog('close');
const river = +elSelected.attr('id').slice(5);
Rivers.remove(river);
elSelected.remove();
$('#riverEditor').dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function closeRiverEditor() {
exitRiverCreationMode();
elSelected.on('click', null);
debug.select('#controlPoints').remove();
debug.select('#controlCells').remove();
unselect();
clearMainTip();
const forced = +document.getElementById('toggleCells').dataset.forced;
document.getElementById('toggleCells').dataset.forced = 0;
if (forced && layerIsOn('toggleCells')) toggleCells();
}
}

View file

@ -21,6 +21,7 @@ function overviewRivers() {
// add listeners
document.getElementById('riversOverviewRefresh').addEventListener('click', riversOverviewAddLines);
document.getElementById('addNewRiver').addEventListener('click', toggleAddRiver);
document.getElementById('riverCreateNew').addEventListener('click', createRiver);
document.getElementById('riversBasinHighlight').addEventListener('click', toggleBasinsHightlight);
document.getElementById('riversExport').addEventListener('click', downloadRiversData);
document.getElementById('riversRemoveAll').addEventListener('click', triggerAllRiversRemove);
@ -129,27 +130,53 @@ function overviewRivers() {
}
function openRiverEditor() {
editRiver('river' + this.parentNode.dataset.id);
const id = 'river' + this.parentNode.dataset.id;
editRiver(id);
}
function triggerRiverRemove() {
const river = +this.parentNode.dataset.id;
alertMessage.innerHTML = `Are you sure you want to remove the river?
All tributaries will be auto-removed`;
const message = 'Are you sure you want to remove the river? <br>All tributaries will be auto-removed';
const onConfirm = () => {
Rivers.remove(river);
riversOverviewAddLines();
};
confirmationDialog({title: 'Remove river', message, confirm: 'Remove', onConfirm});
$('#alert').dialog({
resizable: false,
width: '22em',
title: 'Remove river',
buttons: {
Remove: function () {
Rivers.remove(river);
riversOverviewAddLines();
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function triggerAllRiversRemove() {
const message = 'Are you sure you want to remove all rivers? <br>This action cannot be reverted';
const onConfirm = () => {
pack.rivers = [];
rivers.selectAll('*').remove();
riversOverviewAddLines();
};
confirmationDialog({title: 'Remove all rivers', message, confirm: 'Remove', onConfirm});
alertMessage.innerHTML = `Are you sure you want to remove all rivers?`;
$('#alert').dialog({
resizable: false,
title: 'Remove all rivers',
buttons: {
Remove: function () {
$(this).dialog('close');
removeAllRivers();
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function removeAllRivers() {
pack.rivers = [];
pack.cells.r = new Uint16Array(pack.cells.i.length);
rivers.selectAll('*').remove();
riversOverviewAddLines();
}
}

View file

@ -849,18 +849,21 @@ function editStates() {
}
function adjustProvinces(affectedProvinces) {
const cells = pack.cells,
provinces = pack.provinces,
states = pack.states;
const {cells, provinces, states} = pack;
const form = {Zone: 1, Area: 1, Territory: 2, Province: 1};
<<<<<<< HEAD
affectedProvinces.forEach((p) => {
// do nothing if neutral lands are captured
if (!p) return;
=======
affectedProvinces.forEach(p => {
if (!p) return; // do nothing if neutral lands are captured
const old = provinces[p].state;
>>>>>>> 597f9ae038fbcc149315df9b1618e64744fb929d
// remove province from state provinces list
const old = provinces[p].state;
if (states[old].provinces.includes(p)) states[old].provinces.splice(states[old].provinces.indexOf(p), 1);
if (states[old]?.provinces?.includes(p)) states[old].provinces.splice(states[old].provinces.indexOf(p), 1);
// find states owning at least 1 province cell
const provCells = cells.i.filter((i) => cells.province[i] === p);
@ -871,8 +874,13 @@ function editStates() {
if (owner) {
const name = provinces[p].name;
<<<<<<< HEAD
// if province is historical part of abouther state province, unite with old province
const part = states[owner].provinces.find((n) => name.includes(provinces[n].name));
=======
// if province is a historical part of another state's province, unite with old province
const part = states[owner].provinces.find(n => name.includes(provinces[n].name));
>>>>>>> 597f9ae038fbcc149315df9b1618e64744fb929d
if (part) {
provinces[p].removed = true;
provCells.filter((i) => cells.state[i] === owner).forEach((i) => (cells.province[i] = part));

File diff suppressed because one or more lines are too long

View file

@ -551,94 +551,120 @@ function toggleAddRiver() {
}
function addRiverOnClick() {
const cells = pack.cells;
const point = d3.mouse(this);
let i = findCell(point[0], point[1]);
if (cells.r[i] || cells.h[i] < 20 || cells.b[i]) return;
const {cells, rivers} = pack;
let i = findCell(...d3.mouse(this));
const dataRiver = []; // to store river points
let river = +getNextId('river').slice(5); // river id
cells.fl[i] = grid.cells.prec[cells.g[i]]; // initial flux
if (cells.r[i]) return tip('There is already a river here', false, 'error');
if (cells.h[i] < 20) return tip('Cannot create river in water cell', false, 'error');
if (cells.b[i]) return;
const h = Rivers.alterHeights();
Lakes.prepareLakeData(h);
Rivers.resolveDepressions(h);
const {alterHeights, resolveDepressions, addMeandering, getRiverPath, getBasin, getName, getType, getWidth, getOffset, getApproximateLength} = Rivers;
const riverCells = [];
let riverId = rivers.length ? last(rivers).i + 1 : 1;
let parent = riverId;
const initialFlux = grid.cells.prec[cells.g[i]];
cells.fl[i] = initialFlux;
const h = alterHeights();
resolveDepressions(h);
while (i) {
cells.r[i] = river;
const [x, y] = cells.p[i];
dataRiver.push({x, y, cell: i});
cells.r[i] = riverId;
riverCells.push(i);
const min = cells.c[i].sort((a, b) => h[a] - h[b])[0]; // downhill cell
if (h[i] <= h[min]) return tip(`Cell ${i} is depressed, river cannot flow further`, false, 'error');
const [tx, ty] = cells.p[min];
// pour to water body
if (h[min] < 20) {
// pour to water body
dataRiver.push({x: tx, y: ty, cell: i});
riverCells.push(min);
const feature = pack.features[cells.f[min]];
if (feature.type === 'lake') {
if (feature.outlet) parent = feature.outlet;
feature.inlets ? feature.inlets.push(riverId) : (feature.inlets = [riverId]);
}
break;
}
// pour outside of map from border cell
if (cells.b[min]) {
cells.fl[min] += cells.fl[i];
riverCells.push(-1);
break;
}
// continue propagation if min cell has no river
if (!cells.r[min]) {
// continue if next cell has not river
cells.fl[min] += cells.fl[i];
i = min;
continue;
}
// handle case when lowest cell already has a river
const r = cells.r[min];
const riverCells = cells.i.filter((i) => cells.r[i] === r);
const riverCellsUpper = riverCells.filter((i) => h[i] > h[min]);
const oldRiverId = cells.r[min];
const oldRiver = rivers.find((river) => river.i === oldRiverId);
const oldRiverCells = oldRiver?.cells || cells.i.filter((i) => cells.r[i] === oldRiverId);
const oldRiverCellsUpper = oldRiverCells.filter((i) => h[i] > h[min]);
// finish new river if old river is longer
if (dataRiver.length <= riverCellsUpper.length) {
// create new river as a tributary
if (riverCells.length <= oldRiverCellsUpper.length) {
cells.conf[min] += cells.fl[i];
dataRiver.push({x: tx, y: ty, cell: min});
dataRiver[0].parent = r; // new river is tributary
riverCells.push(min);
parent = oldRiverId;
break;
}
// extend old river
rivers.select('#river' + r).remove();
cells.i.filter((i) => cells.r[i] === river).forEach((i) => (cells.r[i] = r));
riverCells.forEach((i) => (cells.r[i] = 0));
river = r;
cells.fl[min] = cells.fl[i] + grid.cells.prec[cells.g[min]];
i = min;
// continue old river
document.getElementById('river' + oldRiverId)?.remove();
riverCells.forEach((i) => (cells.r[i] = oldRiverId));
oldRiverCells.forEach((cell) => {
if (h[cell] > h[min]) {
cells.r[cell] = 0;
cells.fl[cell] = grid.cells.prec[cells.g[cell]];
} else {
riverCells.push(cell);
cells.fl[cell] += cells.fl[i];
}
});
riverId = oldRiverId;
break;
}
const points = Rivers.addMeandering(dataRiver, 1, 0.5);
const widthFactor = rn(0.8 + Math.random() * 0.4, 1); // river width modifier [.8, 1.2]
const sourceWidth = 0.1;
const [path, length, offset] = Rivers.getPath(points, widthFactor, sourceWidth);
rivers
.append('path')
.attr('d', path)
.attr('id', 'river' + river);
const river = rivers.find((r) => r.i === riverId);
// add new river to data or change extended river attributes
const r = pack.rivers.find((r) => r.i === river);
const mouth = last(dataRiver).cell;
const discharge = cells.fl[mouth]; // in m3/s
const source = riverCells[0];
const mouth = riverCells[riverCells.length - 2];
const widthFactor = river?.widthFactor || (!parent || parent === riverId ? 1.2 : 1);
const meanderedPoints = addMeandering(riverCells);
if (r) {
r.source = dataRiver[0].cell;
r.length = length;
r.discharge = discharge;
const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor));
if (river) {
river.source = source;
river.length = length;
river.discharge = discharge;
river.width = width;
river.cells = riverCells;
} else {
const parent = dataRiver[0].parent || 0;
const basin = Rivers.getBasin(river);
const source = dataRiver[0].cell;
const width = rn(offset ** 2, 2); // mounth width in km
const name = Rivers.getName(mouth);
const smallLength = pack.rivers.map((r) => r.length || 0).sort((a, b) => a - b)[Math.ceil(pack.rivers.length * 0.15)];
const type = length < smallLength ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : 'River';
const basin = getBasin(parent);
const name = getName(mouth);
const type = getType({i: riverId, length, parent});
pack.rivers.push({i: river, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, basin, name, type});
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells, basin, name, type});
}
// render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const path = getRiverPath(meanderedPoints, widthFactor);
const id = 'river' + riverId;
const riversG = viewbox.select('#rivers');
riversG.append('path').attr('id', id).attr('d', path);
if (d3.event.shiftKey === false) {
Lakes.cleanupLakeData();
unpressClickToAddButton();

View file

@ -1,4 +1,5 @@
"use strict";
function editZones() {
closeDialogs();
if (!layerIsOn("toggleZones")) toggleZones();