Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 119 additions & 80 deletions public/modules/io/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,71 +3,103 @@

async function exportToSvg() {
TIME && console.time("exportToSvg");
const url = await getMapURL("svg", {fullMap: true});
const link = document.createElement("a");
link.download = getFileName() + ".svg";
link.href = url;
link.click();

const message = `${link.download} is saved. Open 'Downloads' screen (ctrl + J) to check`;
tip(message, true, "success", 5000);
TIME && console.timeEnd("exportToSvg");
try {
const url = await getMapURL("svg", {fullMap: true});
const link = document.createElement("a");
link.download = getFileName() + ".svg";
link.href = url;
link.click();

const message = `${link.download} is saved. Open 'Downloads' screen (CTRL + J) to check`;
tip(message, true, "success", 5000);
} catch (error) {
ERROR && console.error(error);
tip(`SVG export failed: ${error?.message || "Unknown error"}`, true, "error", 5000);
} finally {
TIME && console.timeEnd("exportToSvg");
}
}

async function exportToPng() {
TIME && console.time("exportToPng");
const url = await getMapURL("png");

const link = document.createElement("a");
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = svgWidth * pngResolutionInput.value;
canvas.height = svgHeight * pngResolutionInput.value;
const img = new Image();
img.src = url;

img.onload = function () {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
link.download = getFileName() + ".png";
canvas.toBlob(function (blob) {
link.href = window.URL.createObjectURL(blob);
link.click();
window.setTimeout(function () {
canvas.remove();
window.URL.revokeObjectURL(link.href);

const message = `${link.download} is saved. Open 'Downloads' screen (ctrl + J) to check. You can set image scale in options`;
tip(message, true, "success", 5000);
}, 1000);
try {
const url = await getMapURL("png");
const link = document.createElement("a");
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = svgWidth * pngResolutionInput.value;
canvas.height = svgHeight * pngResolutionInput.value;

const blob = await new Promise((resolve, reject) => {
const img = new Image();
img.onload = function () {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob(blob => {
if (!blob) return reject(new Error("Cannot render PNG image"));
resolve(blob);
}, "image/png");
};
img.onerror = () => reject(new Error("Cannot load map image for PNG export"));
img.src = url;
});
};

TIME && console.timeEnd("exportToPng");
link.download = getFileName() + ".png";
link.href = window.URL.createObjectURL(blob);
link.click();
window.setTimeout(function () {
canvas.remove();
window.URL.revokeObjectURL(link.href);
}, 1000);

const message = `${link.download} is saved. Open 'Downloads' screen (CTRL + J) to check. You can set image scale in options`;
tip(message, true, "success", 5000);
} catch (error) {
ERROR && console.error(error);
tip(`PNG export failed: ${error?.message || "Unknown error"}`, true, "error", 5000);
} finally {
TIME && console.timeEnd("exportToPng");
}
}

async function exportToJpeg() {
TIME && console.time("exportToJpeg");
const url = await getMapURL("png");
try {
const url = await getMapURL("png");
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = svgWidth * pngResolutionInput.value;
canvas.height = svgHeight * pngResolutionInput.value;

const canvas = document.createElement("canvas");
canvas.width = svgWidth * pngResolutionInput.value;
canvas.height = svgHeight * pngResolutionInput.value;
const img = new Image();
img.src = url;

img.onload = async function () {
canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
const quality = Math.min(rn(1 - pngResolutionInput.value / 20, 2), 0.92);
const URL = await canvas.toDataURL("image/jpeg", quality);
const blob = await new Promise((resolve, reject) => {
const img = new Image();
img.onload = function () {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob(
blob => {
if (!blob) return reject(new Error("Cannot render JPEG image"));
resolve(blob);
},
"image/jpeg",
quality
);
};
img.onerror = () => reject(new Error("Cannot load map image for JPEG export"));
img.src = url;
});

const link = document.createElement("a");
link.download = getFileName() + ".jpeg";
link.href = URL;
link.href = window.URL.createObjectURL(blob);
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000);
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
};

TIME && console.timeEnd("exportToJpeg");
window.setTimeout(() => window.URL.revokeObjectURL(link.href), 5000);
} catch (error) {
ERROR && console.error(error);
tip(`JPEG export failed: ${error?.message || "Unknown error"}`, true, "error", 5000);
} finally {
TIME && console.timeEnd("exportToJpeg");
}
}

async function exportToPngTiles() {
Expand Down Expand Up @@ -132,17 +164,24 @@ async function exportToPngTiles() {
}

status.innerHTML = "Zipping files...";
zip.generateAsync({type: "blob"}).then(blob => {
status.innerHTML = "Downloading the archive...";
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = getFileName() + ".zip";
link.click();
link.remove();

status.innerHTML = 'Done. Check .zip file in "Downloads" (crtl + J)';
setTimeout(() => URL.revokeObjectURL(link.href), 5000);
});
zip
.generateAsync({type: "blob"})
.then(blob => {
status.innerHTML = "Downloading the archive...";
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = getFileName() + ".zip";
link.click();
link.remove();

status.innerHTML = 'Done. Check .zip file in "Downloads" (CTRL + J)';
setTimeout(() => URL.revokeObjectURL(link.href), 5000);
})
.catch(error => {
ERROR && console.error(error);
status.innerHTML = "Tiles export failed";
tip(`PNG tiles export failed: ${error?.message || "Unknown error"}`, true, "error", 5000);
});

// promisified img.onload
function loadImage(img) {
Expand Down Expand Up @@ -583,70 +622,70 @@ function saveGeoJsonZones() {
// Handles multiple disconnected components and holes properly
function getZonePolygonCoordinates(zoneCells) {
const cellsInZone = new Set(zoneCells);
const ofSameType = (cellId) => cellsInZone.has(cellId);
const ofDifferentType = (cellId) => !cellsInZone.has(cellId);
const ofSameType = cellId => cellsInZone.has(cellId);
const ofDifferentType = cellId => !cellsInZone.has(cellId);

const checkedCells = new Set();
const rings = []; // Array of LinearRings (each ring is an array of coordinates)

// Find all boundary components by tracing each connected region
for (const cellId of zoneCells) {
if (checkedCells.has(cellId)) continue;

// Check if this cell is on the boundary (has a neighbor outside the zone)
const neighbors = cells.c[cellId];
const onBorder = neighbors.some(ofDifferentType);
if (!onBorder) continue;

// Check if this is an inner lake (hole) - skip if so
const feature = pack.features[cells.f[cellId]];
if (feature.type === "lake" && feature.shoreline) {
if (feature.shoreline.every(ofSameType)) continue;
}

// Find a starting vertex that's on the boundary
const cellVertices = cells.v[cellId];
let startingVertex = null;

for (const vertexId of cellVertices) {
const vertexCells = vertices.c[vertexId];
if (vertexCells.some(ofDifferentType)) {
startingVertex = vertexId;
break;
}
}

if (startingVertex === null) continue;

// Use connectVertices to trace the boundary (reusing existing logic)
const vertexChain = connectVertices({
vertices,
startingVertex,
ofSameType,
addToChecked: (cellId) => checkedCells.add(cellId),
closeRing: false, // We'll close it manually after converting to coordinates
addToChecked: cellId => checkedCells.add(cellId),
closeRing: false // We'll close it manually after converting to coordinates
});

if (vertexChain.length < 3) continue;

// Convert vertex chain to coordinates
const coordinates = [];
for (const vertexId of vertexChain) {
const [x, y] = vertices.p[vertexId];
coordinates.push(getCoordinates(x, y, 4));
}

// Close the ring (first coordinate = last coordinate)
if (coordinates.length > 0) {
coordinates.push(coordinates[0]);
}

// Only add ring if it has at least 4 positions (minimum for valid LinearRing)
if (coordinates.length >= 4) {
rings.push(coordinates);
}
}

return rings;
}

Expand All @@ -656,18 +695,18 @@ function saveGeoJsonZones() {
if (zone.hidden || !zone.cells || zone.cells.length === 0) return;

const rings = getZonePolygonCoordinates(zone.cells);

// Skip if no valid rings were generated
if (rings.length === 0) return;

const properties = {
id: zone.i,
name: zone.name,
type: zone.type,
color: zone.color,
cells: zone.cells
};

// If there's only one ring, use Polygon geometry
if (rings.length === 1) {
const feature = {
Expand Down
38 changes: 22 additions & 16 deletions public/modules/io/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,30 @@ function loadMapPrompt(blob) {
}
}

function loadMapFromURL(maplink, random) {
const URL = decodeURIComponent(maplink);

fetch(URL, {method: "GET", mode: "cors"})
.then(response => {
if (response.ok) return response.blob();
throw new Error("Cannot load map from URL");
})
.then(blob => uploadMap(blob))
.catch(error => {
showUploadErrorMessage(error.message, URL, random);
if (random) generateMapOnLoad();
});
async function loadMapFromURL(maplink, random) {
const controller = new AbortController();
const TIMEOUT = 120000; // 120 seconds
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);

try {
const url = decodeURIComponent(maplink);
const response = await fetch(url, {method: "GET", mode: "cors", signal: controller.signal});
if (!response.ok) throw new Error("Cannot load map from URL");

const blob = await response.blob();
uploadMap(blob);
} catch (error) {
const message = error?.name === "AbortError" ? "Cannot load map from URL: request timed out" : error.message;
showUploadErrorMessage(message, maplink, random);
if (random) generateMapOnLoad();
Comment thread
Azgaar marked this conversation as resolved.
} finally {
clearTimeout(timeoutId);
}
}

function showUploadErrorMessage(error, URL, random) {
function showUploadErrorMessage(error, maplink, random) {
ERROR && console.error(error);
alertMessage.innerHTML = /* html */ `Cannot load map from the ${link(URL, "link provided")}. ${
alertMessage.innerHTML = /* html */ `Cannot load map from the ${link(maplink, "link provided")}. ${
random ? `A new random map is generated. ` : ""
} Please ensure the
linked file is reachable and CORS is allowed on server side`;
Expand Down Expand Up @@ -744,7 +750,7 @@ async function parseLoadedData(data, mapVersion) {
ERROR && console.error(error);
clearMainTip();

alertMessage.innerHTML = /* html */ `An error is occured on map loading. Select a different file to load, <br>generate a new random map or cancel the loading.<br>Map version: ${mapVersion}. Generator version: ${VERSION}.
alertMessage.innerHTML = /* html */ `An error occurred while loading the map. Select a different file to load, <br>generate a new random map or cancel the loading.<br>Map version: ${mapVersion}. Generator version: ${VERSION}.
<p id="errorBox">${parseError(error)}</p>`;

$("#alert").dialog({
Expand Down
Loading
Loading