refactor: submap - start

This commit is contained in:
Azgaar 2024-10-13 15:04:45 +02:00
parent 26b659a59e
commit 58ccd238f6
8 changed files with 486 additions and 522 deletions

View file

@ -2,75 +2,51 @@
window.Submap = (function () {
const isWater = (pack, id) => pack.cells.h[id] < 20;
const inMap = (x, y) => x > 0 && x < graphWidth && y > 0 && y < graphHeight;
const inMap = (x, y) => x => 0 && x <= graphWidth && y >= 0 && y <= graphHeight;
/*
generate new map based on an existing one (resampling parentMap)
parentMap: {seed, grid, pack} from original map
parentMap: {grid, pack, notes} from original map
options = {
projection: f(Number,Number)->[Number, Number]
projection: f(Number, Number) -> [Number, Number]
function to calculate new coordinates
inverse: g(Number,Number)->[Number, Number]
inverse: g(Number, Number) -> [Number, Number]
inverse of f
depressRivers: Bool carve out riverbeds?
depressRivers: Bool carve out riverbeds
smoothHeightMap: Bool run smooth filter on heights
addLakesInDepressions: call FMG original funtion on heightmap
lockMarkers: Bool Auto lock all copied markers
lockBurgs: Bool Auto lock all copied burgs
}
*/
function resample(parentMap, options) {
const projection = options.projection;
const inverse = options.inverse;
const stage = s => INFO && console.info("SUBMAP:", s);
const timeStart = performance.now();
invokeActiveZooming();
const {grid: parentGrid, pack: parentPack} = parentMap;
const {projection, inverse} = options;
// copy seed
seed = parentMap.seed;
Math.random = aleaPRNG(seed);
INFO && console.group("SubMap with seed: " + seed);
DEBUG && console.info("Using Options:", options);
applyGraphSize();
grid = generateGrid();
drawScaleBar(scaleBar, scale);
fitScaleBar(scaleBar, svgWidth, svgHeight);
const resampler = (points, qtree, f) => {
for (const [i, [x, y]] of points.entries()) {
const [tx, ty] = inverse(x, y);
const oldid = qtree.find(tx, ty, Infinity)[2];
f(i, oldid);
}
};
stage("Resampling heightmap, temperature and precipitation");
// resample heightmap from old WorldState
const n = grid.points.length;
grid.cells.h = new Uint8Array(n); // heightmap
grid.cells.temp = new Int8Array(n); // temperature
grid.cells.prec = new Uint8Array(n); // precipitation
const reverseGridMap = new Uint32Array(n); // cellmap from new -> oldcell
const forwardGridMap = parentGrid.points.map(_ => []); // old -> [newcelllist]
const oldGrid = parentMap.grid;
// build cache old -> [newcelllist]
const forwardGridMap = parentMap.grid.points.map(_ => []);
resampler(grid.points, parentMap.pack.cells.q, (id, oldid) => {
const cid = parentMap.pack.cells.g[oldid];
grid.cells.h[id] = oldGrid.cells.h[cid];
grid.cells.temp[id] = oldGrid.cells.temp[cid];
grid.cells.prec[id] = oldGrid.cells.prec[cid];
if (options.depressRivers) forwardGridMap[cid].push(id);
reverseGridMap[id] = cid;
});
// TODO: add smooth/noise function for h, temp, prec n times
for (const [gridId, [x, y]] of grid.points.entries()) {
const [parentX, parentY] = inverse(x, y);
const parentPackId = parentPack.cells.q.find(parentX, parentY, Infinity)[2];
const parentGridId = parentPack.cells.g[parentPackId];
// smooth heightmap
// smoothing should never change cell type (land->water or water->land)
grid.cells.h[gridId] = parentGrid.cells.h[parentGridId];
grid.cells.temp[gridId] = parentGrid.cells.temp[parentGridId];
grid.cells.prec[gridId] = parentGrid.cells.prec[parentGridId];
if (options.depressRivers) forwardGridMap[parentGridId].push(gridId);
reverseGridMap[gridId] = parentGridId;
}
// smoothing should not change cell type (land -> water or vice versa)
if (options.smoothHeightMap) {
const gcells = grid.cells;
gcells.h.forEach((h, i) => {
@ -81,7 +57,6 @@ window.Submap = (function () {
}
if (options.depressRivers) {
stage("Generating riverbeds");
const rbeds = new Uint16Array(grid.cells.i.length);
// and erode riverbeds
@ -97,6 +72,7 @@ window.Submap = (function () {
});
})
);
// raise every land cell a bit except riverbeds
grid.cells.h.forEach((h, i) => {
if (rbeds[i] || h < 20) return;
@ -104,7 +80,6 @@ window.Submap = (function () {
});
}
stage("Detect features, ocean and generating lakes");
Features.markupGrid();
addLakesInDeepDepressions();
@ -115,11 +90,8 @@ window.Submap = (function () {
calculateMapCoordinates();
calculateTemperatures();
generatePrecipitation();
stage("Cell cleanup");
reGraph();
// remove misclassified cells
stage("Define coastline");
Features.markupPack();
createDefaultRuler();
@ -135,7 +107,6 @@ window.Submap = (function () {
cells.religion = new Uint16Array(pn);
cells.province = new Uint16Array(pn);
stage("Resampling culture, state and religion map");
for (const [id, gridCellId] of cells.g.entries()) {
const oldGridId = reverseGridMap[gridCellId];
if (oldGridId === undefined) {
@ -148,8 +119,8 @@ window.Submap = (function () {
if (!oldChildren.length) {
// it *must* be a (deleted) deep ocean cell
if (!oldGrid.cells.h[oldGridId] < 20) {
console.error(`Warning, ${gridCellId} should be water cell, not ${oldGrid.cells.h[oldGridId]}`);
if (!parentGrid.cells.h[oldGridId] < 20) {
console.error(`Warning, ${gridCellId} should be water cell, not ${parentGrid.cells.h[oldGridId]}`);
continue;
}
// find replacement: closest water cell
@ -166,9 +137,9 @@ window.Submap = (function () {
let d = Infinity;
oldChildren.forEach(oid => {
// this should be always true, unless some algo modded the height!
if (isWater(parentMap.pack, oid) !== isWater(pack, id)) {
if (isWater(parentMap.pack, oid) !== isWater(pack, id))
console.warn(`cell sank because of addLakesInDepressions: ${oid}`);
}
const [oldpx, oldpy] = oldCells.p[oid];
const nd = distance(projection(oldpx, oldpy));
if (isNaN(nd)) {
@ -194,18 +165,15 @@ window.Submap = (function () {
forwardMap[oldid].push(id);
}
stage("Regenerating river network");
Rivers.generate();
// biome calculation based on (resampled) grid.cells.temp and prec
// it's safe to recalculate.
stage("Regenerating Biome");
Biomes.define();
// recalculate suitability and population
// TODO: normalize according to the base-map
rankCells();
stage("Porting Cultures");
pack.cultures = parentMap.pack.cultures;
// fix culture centers
const validCultures = new Set(pack.cells.culture);
@ -220,11 +188,9 @@ window.Submap = (function () {
c.center = newCenters.length ? newCenters[0] : pack.cells.culture.findIndex(x => x === i);
});
stage("Porting and locking burgs");
copyBurgs(parentMap, projection, options);
// transfer states, mark states without land as removed.
stage("Porting states");
const validStates = new Set(pack.cells.state);
pack.states = parentMap.pack.states;
// keep valid states and neighbors only
@ -241,7 +207,6 @@ window.Submap = (function () {
BurgsAndStates.getPoles();
// transfer provinces, mark provinces without land as removed.
stage("Porting provinces");
const validProvinces = new Set(pack.cells.province);
pack.provinces = parentMap.pack.provinces;
// mark uneccesary provinces
@ -255,14 +220,11 @@ window.Submap = (function () {
p.center = newCenters.length ? newCenters[0] : pack.cells.province.findIndex(x => x === i);
});
Provinces.getPoles();
stage("Regenerating routes network");
regenerateRoutes();
Rivers.specify();
Features.specify();
stage("Porting military");
for (const s of pack.states) {
if (!s.military) continue;
for (const m of s.military) {
@ -274,7 +236,6 @@ window.Submap = (function () {
s.military = s.military.filter(m => m.cell).map((m, i) => ({...m, i}));
}
stage("Copying markers");
for (const m of pack.markers) {
const [x, y] = projection(m.x, m.y);
if (!inMap(x, y)) {
@ -288,16 +249,11 @@ window.Submap = (function () {
}
if (layerIsOn("toggleMarkers")) drawMarkers();
stage("Regenerating Zones");
Zones.generate();
Names.getMapName();
stage("Restoring Notes");
notes = parentMap.notes;
stage("Submap done");
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
showStatistics();
INFO && console.groupEnd("Generated Map " + seed);
}
/* find the nearest cell accepted by filter f *and* having at
@ -403,6 +359,5 @@ window.Submap = (function () {
return [x, y];
}
// export
return {resample, findNearest};
return {resample};
})();

92
modules/ui/submap-tool.js Normal file
View file

@ -0,0 +1,92 @@
"use strict";
function openSubmapTool() {
$("#submapTool").dialog({
title: "Create a submap",
resizable: false,
width: "32em",
position: {my: "center", at: "center", of: "svg"},
buttons: {
Submap: function () {
closeDialogs();
generateSubmap();
},
Cancel: function () {
$(this).dialog("close");
}
}
});
if (modules.openSubmapMenu) return;
modules.openSubmapMenu = true;
async function generateSubmap() {
INFO && console.group("generateSubmap");
const [[x0, y0]] = getViewBoxExtent();
// converting map position on the planet. TODO: fix, coordinates are wrong
const mapSizeOutput = byId("mapSizeOutput");
const latitudeOutput = byId("latitudeOutput");
const latN = 90 - ((180 - (mapSizeInput.value / 100) * 180) * latitudeOutput.value) / 100;
const newLatN = latN - ((y0 / graphHeight) * mapSizeOutput.value * 180) / 100;
mapSizeOutput.value /= scale;
latitudeOutput.value = ((90 - newLatN) / (180 - (mapSizeOutput.value / 100) * 180)) * 100;
byId("mapSizeInput").value = mapSizeOutput.value;
byId("latitudeInput").value = latitudeOutput.value;
distanceScale = distanceScaleInput.value = rn(distanceScaleInput.value / scale, 2);
populationRate = populationRateInput.value = rn(populationRateInput.value / scale, 2);
const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)};
const options = {
lockMarkers: byId("submapLockMarkers").checked,
lockBurgs: byId("submapLockBurgs").checked,
depressRivers: byId("submapDepressRivers").checked,
addLakesInDepressions: byId("submapAddLakeInDepression").checked,
smoothHeightMap: scale > 2,
inverse: (x, y) => [x / scale + x0, y / scale + y0],
projection: (x, y) => [(x - x0) * scale, (y - y0) * scale],
scale
};
resetZoom(0);
undraw();
const oldScale = scale;
await Submap.resample(parentMap, options);
if (byId("submapPromoteTowns").checked) {
const groupName = "largetowns";
moveAllBurgsToGroup("towns", groupName);
changeRadius(rn(oldScale * 0.8, 2), groupName);
changeFontSize(svg.select(`#labels #${groupName}`), rn(oldScale * 2, 2));
invokeActiveZooming();
}
if (byId("submapRescaleStyles").checked) changeStyles(oldScale);
drawLayers();
INFO && console.groupEnd("generateSubmap");
}
function changeStyles(scale) {
// resize burgIcons
const burgIcons = [...byId("burgIcons").querySelectorAll("g")];
for (const bi of burgIcons) {
const newRadius = rn(minmax(bi.getAttribute("size") * scale, 0.2, 10), 2);
changeRadius(newRadius, bi.id);
const swAttr = bi.attributes["stroke-width"];
swAttr.value = +swAttr.value * scale;
}
// burglabels
const burgLabels = [...byId("burgLabels").querySelectorAll("g")];
for (const bl of burgLabels) {
const size = +bl.dataset["size"];
bl.dataset["size"] = Math.max(rn((size + size / scale) / 2, 2), 1) * scale;
}
drawEmblems();
}
}

View file

@ -1,332 +0,0 @@
"use strict";
// UI elements for submap generation
window.UISubmap = (function () {
byId("submapPointsInput").addEventListener("input", function () {
const output = byId("submapPointsOutputFormatted");
const cells = cellsDensityMap[+this.value] || 1000;
this.dataset.cells = cells;
output.value = getCellsDensityValue(cells);
output.style.color = getCellsDensityColor(cells);
});
byId("submapScaleInput").addEventListener("input", function (event) {
const exp = Math.pow(1.1, +event.target.value);
byId("submapScaleOutput").value = rn(exp, 2);
});
byId("submapAngleInput").addEventListener("input", function (event) {
byId("submapAngleOutput").value = event.target.value;
});
const $previewBox = byId("submapPreview");
const $scaleInput = byId("submapScaleInput");
const $shiftX = byId("submapShiftX");
const $shiftY = byId("submapShiftY");
function openSubmapMenu() {
$("#submapOptionsDialog").dialog({
title: "Create a submap",
resizable: false,
width: "32em",
position: {my: "center", at: "center", of: "svg"},
buttons: {
Submap: function () {
$(this).dialog("close");
generateSubmap();
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
const getTransformInput = _ => ({
angle: (+byId("submapAngleInput").value / 180) * Math.PI,
shiftX: +byId("submapShiftX").value,
shiftY: +byId("submapShiftY").value,
ratio: +byId("submapScaleInput").value,
mirrorH: byId("submapMirrorH").checked,
mirrorV: byId("submapMirrorV").checked
});
async function openResampleMenu() {
resetZoom(0);
byId("submapAngleInput").value = 0;
byId("submapAngleOutput").value = "0";
byId("submapScaleOutput").value = 1;
byId("submapMirrorH").checked = false;
byId("submapMirrorV").checked = false;
$scaleInput.value = 0;
$shiftX.value = 0;
$shiftY.value = 0;
const w = Math.min(400, window.innerWidth * 0.5);
const previewScale = w / graphWidth;
const h = graphHeight * previewScale;
$previewBox.style.width = w + "px";
$previewBox.style.height = h + "px";
// handle mouse input
const dispatchInput = e => e.dispatchEvent(new Event("input", {bubbles: true}));
// mouse wheel
$previewBox.onwheel = e => {
$scaleInput.value = $scaleInput.valueAsNumber - Math.sign(e.deltaY);
dispatchInput($scaleInput);
};
// mouse drag
let mouseIsDown = false,
mouseX = 0,
mouseY = 0;
$previewBox.onmousedown = e => {
mouseIsDown = true;
mouseX = $shiftX.value - e.clientX / previewScale;
mouseY = $shiftY.value - e.clientY / previewScale;
};
$previewBox.onmouseup = _ => (mouseIsDown = false);
$previewBox.onmouseleave = _ => (mouseIsDown = false);
$previewBox.onmousemove = e => {
if (!mouseIsDown) return;
e.preventDefault();
$shiftX.value = Math.round(mouseX + e.clientX / previewScale);
$shiftY.value = Math.round(mouseY + e.clientY / previewScale);
dispatchInput($shiftX);
// dispatchInput($shiftY); // not needed X bubbles anyway
};
$("#resampleDialog").dialog({
title: "Transform map",
resizable: false,
position: {my: "center", at: "center", of: "svg"},
buttons: {
Transform: function () {
$(this).dialog("close");
resampleCurrentMap();
},
Cancel: function () {
$(this).dialog("close");
}
}
});
// use double resolution for PNG to get sharper image
const $preview = await loadPreview($previewBox, w * 2, h * 2);
// could be done with SVG. Faster to load, slower to use.
// const $preview = await loadPreviewSVG($previewBox, w, h);
$preview.style.position = "absolute";
$preview.style.width = w + "px";
$preview.style.height = h + "px";
byId("resampleDialog").oninput = event => {
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
const scale = Math.pow(1.1, ratio);
const transformStyle = `
translate(${shiftX * previewScale}px, ${shiftY * previewScale}px)
scale(${mirrorH ? -scale : scale}, ${mirrorV ? -scale : scale})
rotate(${angle}rad)
`;
$preview.style.transform = transformStyle;
$preview.style["transform-origin"] = "center";
event.stopPropagation();
};
}
async function loadPreview($container, w, h) {
const url = await getMapURL("png", {
globe: false,
noWater: true,
fullMap: true,
noLabels: true,
noScaleBar: true,
noVignette: true,
noIce: true
});
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = w;
canvas.height = h;
const img = new Image();
img.src = url;
img.onload = function () {
ctx.drawImage(img, 0, 0, w, h);
};
$container.textContent = "";
$container.appendChild(canvas);
return canvas;
}
// Resample the whole map to different cell resolution or shape
const resampleCurrentMap = debounce(function () {
WARN && console.warn("Resampling current map");
const cellNumId = +byId("submapPointsInput").value;
if (!cellsDensityMap[cellNumId]) return console.error("Unknown cell number!");
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
const [cx, cy] = [graphWidth / 2, graphHeight / 2];
const rot = alfa => (x, y) =>
[
(x - cx) * Math.cos(alfa) - (y - cy) * Math.sin(alfa) + cx,
(y - cy) * Math.cos(alfa) + (x - cx) * Math.sin(alfa) + cy
];
const shift = (dx, dy) => (x, y) => [x + dx, y + dy];
const scale = r => (x, y) => [(x - cx) * r + cx, (y - cy) * r + cy];
const flipH = (x, y) => [-x + 2 * cx, y];
const flipV = (x, y) => [x, -y + 2 * cy];
const app = (f, g) => (x, y) => f(...g(x, y));
const id = (x, y) => [x, y];
let projection = id;
let inverse = id;
if (angle) [projection, inverse] = [rot(angle), rot(-angle)];
if (ratio)
[projection, inverse] = [
app(scale(Math.pow(1.1, ratio)), projection),
app(inverse, scale(Math.pow(1.1, -ratio)))
];
if (mirrorH) [projection, inverse] = [app(flipH, projection), app(inverse, flipH)];
if (mirrorV) [projection, inverse] = [app(flipV, projection), app(inverse, flipV)];
if (shiftX || shiftY) {
projection = app(shift(shiftX, shiftY), projection);
inverse = app(inverse, shift(-shiftX, -shiftY));
}
changeCellsDensity(cellNumId);
startResample({
lockMarkers: false,
lockBurgs: false,
depressRivers: false,
addLakesInDepressions: false,
promoteTowns: false,
smoothHeightMap: false,
rescaleStyles: false,
scale: 1,
projection,
inverse
});
}, 1000);
// Create submap from the current map. Submap limits defined by the current window size (canvas viewport)
const generateSubmap = debounce(function () {
WARN && console.warn("Resampling current map");
closeDialogs("#worldConfigurator, #options3d");
const checked = id => Boolean(byId(id).checked);
// Create projection func from current zoom extents
const [[x0, y0], [x1, y1]] = getViewBoxExtent();
const origScale = scale;
const options = {
lockMarkers: checked("submapLockMarkers"),
lockBurgs: checked("submapLockBurgs"),
depressRivers: checked("submapDepressRivers"),
addLakesInDepressions: checked("submapAddLakeInDepression"),
promoteTowns: checked("submapPromoteTowns"),
rescaleStyles: checked("submapRescaleStyles"),
smoothHeightMap: scale > 2,
inverse: (x, y) => [x / origScale + x0, y / origScale + y0],
projection: (x, y) => [(x - x0) * origScale, (y - y0) * origScale],
scale: origScale
};
// converting map position on the planet
const mapSizeOutput = byId("mapSizeOutput");
const latitudeOutput = byId("latitudeOutput");
const latN = 90 - ((180 - (mapSizeInput.value / 100) * 180) * latitudeOutput.value) / 100;
const newLatN = latN - ((y0 / graphHeight) * mapSizeOutput.value * 180) / 100;
mapSizeOutput.value /= scale;
latitudeOutput.value = ((90 - newLatN) / (180 - (mapSizeOutput.value / 100) * 180)) * 100;
byId("mapSizeInput").value = mapSizeOutput.value;
byId("latitudeInput").value = latitudeOutput.value;
// fix scale
distanceScale = distanceScaleInput.value = rn(distanceScaleInput.value / scale, 2);
populationRate = populationRateInput.value = rn(populationRateInput.value / scale, 2);
customization = 0;
startResample(options);
}, 1000);
async function startResample(options) {
// Do model changes with Submap.resample then do view changes if needed
resetZoom(0);
let oldstate = {
grid: deepCopy(grid),
pack: deepCopy(pack),
notes: deepCopy(notes),
seed,
graphWidth,
graphHeight
};
undraw();
try {
const oldScale = scale;
await Submap.resample(oldstate, options);
if (options.promoteTowns) {
const groupName = "largetowns";
moveAllBurgsToGroup("towns", groupName);
changeRadius(rn(oldScale * 0.8, 2), groupName);
changeFontSize(svg.select(`#labels #${groupName}`), rn(oldScale * 2, 2));
invokeActiveZooming();
}
if (options.rescaleStyles) changeStyles(oldScale);
} catch (error) {
showSubmapErrorHandler(error);
}
oldstate = null; // destroy old state to free memory
drawLayers();
if (ThreeD.options.isOn) ThreeD.redraw();
if ($("#worldConfigurator").is(":visible")) editWorld();
}
function changeStyles(scale) {
// resize burgIcons
const burgIcons = [...byId("burgIcons").querySelectorAll("g")];
for (const bi of burgIcons) {
const newRadius = rn(minmax(bi.getAttribute("size") * scale, 0.2, 10), 2);
changeRadius(newRadius, bi.id);
const swAttr = bi.attributes["stroke-width"];
swAttr.value = +swAttr.value * scale;
}
// burglabels
const burgLabels = [...byId("burgLabels").querySelectorAll("g")];
for (const bl of burgLabels) {
const size = +bl.dataset["size"];
bl.dataset["size"] = Math.max(rn((size + size / scale) / 2, 2), 1) * scale;
}
drawEmblems();
}
function showSubmapErrorHandler(error) {
ERROR && console.error(error);
clearMainTip();
alertMessage.innerHTML = /* html */ `Map resampling failed: <br />You may retry after clearing stored data or contact us at discord.
<p id="errorBox">${parseError(error)}</p>`;
$("#alert").dialog({
resizable: false,
title: "Resampling error",
width: "32em",
buttons: {
Ok: function () {
$(this).dialog("close");
}
},
position: {my: "center", at: "center", of: "svg"}
});
}
return {openSubmapMenu, openResampleMenu};
})();

View file

@ -3,7 +3,7 @@
// module to control the Tools options (click to edit, to re-geenerate, tp add)
toolsContent.addEventListener("click", function (event) {
if (customization) return tip("Please exit the customization mode first", false, "warning");
if (customization) return tip("Please exit the customization mode first", false, "error");
if (!["BUTTON", "I"].includes(event.target.tagName)) return;
const button = event.target.id;
@ -70,8 +70,8 @@ toolsContent.addEventListener("click", function (event) {
else if (button === "addRoute") createRoute();
else if (button === "addMarker") toggleAddMarker();
// click to create a new map buttons
else if (button === "openSubmapMenu") UISubmap.openSubmapMenu();
else if (button === "openResampleMenu") UISubmap.openResampleMenu();
else if (button === "openSubmapTool") openSubmapTool();
else if (button === "openTransformTool") openTransformTool();
});
function processFeatureRegeneration(event, button) {

View file

@ -0,0 +1,207 @@
"use strict";
async function openTransformTool() {
const width = Math.min(400, window.innerWidth * 0.5);
const previewScale = width / graphWidth;
const height = graphHeight * previewScale;
let mouseIsDown = false;
let mouseX = 0;
let mouseY = 0;
resetInputs();
loadPreview();
$("#transformTool").dialog({
title: "Transform map",
resizable: false,
position: {my: "center", at: "center", of: "svg"},
buttons: {
Transform: function () {
closeDialogs();
transformMap();
},
Cancel: function () {
$(this).dialog("close");
}
}
});
if (modules.openTransformTool) return;
modules.openTransformTool = true;
// add listeners
byId("transformToolBody").on("input", handleInput);
byId("transformPointsInput").on("input", handleCellsChange);
byId("transformPreview")
.on("mousedown", handleMousedown)
.on("mouseup", _ => (mouseIsDown = false))
.on("mousemove", handleMousemove)
.on("wheel", handleWheel);
async function loadPreview() {
byId("transformPreview").style.width = width + "px";
byId("transformPreview").style.height = height + "px";
const options = {noWater: true, fullMap: true, noLabels: true, noScaleBar: true, noVignette: true, noIce: true};
const url = await getMapURL("png", options);
const img = new Image();
img.src = url;
img.onload = function () {
const $canvas = byId("transformPreviewCanvas");
$canvas.style.width = width + "px";
$canvas.style.height = height + "px";
$canvas.width = width * 2;
$canvas.height = height * 2;
$canvas.getContext("2d").drawImage(img, 0, 0, width * 2, height * 2);
};
}
function resetInputs() {
byId("transformAngleInput").value = 0;
byId("transformAngleOutput").value = "0";
byId("transformMirrorH").checked = false;
byId("transformMirrorV").checked = false;
byId("transformScaleInput").value = 0;
byId("transformScaleResult").value = 1;
byId("transformShiftX").value = 0;
byId("transformShiftY").value = 0;
handleInput();
}
function handleInput() {
const angle = (+byId("transformAngleInput").value / 180) * Math.PI;
const shiftX = +byId("transformShiftX").value;
const shiftY = +byId("transformShiftY").value;
const mirrorH = byId("transformMirrorH").checked;
const mirrorV = byId("transformMirrorV").checked;
const EXP = 1.0965;
const scale = rn(EXP ** +byId("transformScaleInput").value, 2); // [0.1, 10]x
byId("transformScaleResult").value = scale;
byId("transformPreviewCanvas").style.transform = `
translate(${shiftX * previewScale}px, ${shiftY * previewScale}px)
scale(${mirrorH ? -scale : scale}, ${mirrorV ? -scale : scale})
rotate(${angle}rad)
`;
}
function handleCellsChange() {
const cells = cellsDensityMap[+this.value] || 1000;
this.dataset.cells = cells;
const output = byId("transformPointsFormatted");
output.value = getCellsDensityValue(cells);
output.style.color = getCellsDensityColor(cells);
}
function handleMousedown(e) {
mouseIsDown = true;
const shiftX = +byId("transformShiftX").value;
const shiftY = +byId("transformShiftY").value;
mouseX = shiftX - e.clientX / previewScale;
mouseY = shiftY - e.clientY / previewScale;
}
function handleMousemove(e) {
if (!mouseIsDown) return;
e.preventDefault();
byId("transformShiftX").value = Math.round(mouseX + e.clientX / previewScale);
byId("transformShiftY").value = Math.round(mouseY + e.clientY / previewScale);
handleInput();
}
function handleWheel(e) {
const $scaleInput = byId("transformScaleInput");
$scaleInput.value = $scaleInput.valueAsNumber - Math.sign(e.deltaY);
handleInput();
}
async function transformMap() {
INFO && console.group("transformMap");
const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)};
const [projection, inverse] = getProjection();
const options = {
lockMarkers: false,
lockBurgs: false,
depressRivers: false,
addLakesInDepressions: false,
smoothHeightMap: false,
scale: 1,
inverse,
projection
};
const cellsNumber = +byId("transformPointsInput").value;
changeCellsDensity(cellsNumber);
resetZoom(0);
undraw();
await Submap.resample(parentMap, options);
drawLayers();
INFO && console.groupEnd("transformMap");
}
function getProjection() {
const centerX = graphWidth / 2;
const centerY = graphHeight / 2;
const shiftX = +byId("transformShiftX").value;
const shiftY = +byId("transformShiftY").value;
const angle = (+byId("transformAngleInput").value / 180) * Math.PI;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const scale = +byId("transformScaleResult").value;
const mirrorH = byId("transformMirrorH").checked;
const mirrorV = byId("transformMirrorV").checked;
function project(x, y) {
// center the point
x -= centerX;
y -= centerY;
// apply scale
if (scale !== 1) {
x *= scale;
y *= scale;
}
// apply rotation
if (angle) [x, y] = [x * cos - y * sin, x * sin + y * cos];
// apply mirroring
if (mirrorH) x = -x;
if (mirrorV) y = -y;
// uncenter the point and apply shift
return [x + centerX + shiftX, y + centerY + shiftY];
}
function inverse(x, y) {
// undo shift and center the point
x -= centerX + shiftX;
y -= centerY + shiftY;
// undo mirroring
if (mirrorV) y = -y;
if (mirrorH) x = -x;
// undo rotation
if (angle !== 0) [x, y] = [x * cos + y * sin, -x * sin + y * cos];
// undo scale
if (scale !== 1) {
x /= scale;
y /= scale;
}
// uncenter the point
return [x + centerX, y + centerY];
}
return [project, inverse];
}
}