Skip to content

Commit beb4f52

Browse files
AzgaarCopilot
andauthored
Minor fixes (#1388)
* fix: improve error handling in AI provider response and update setTimeout syntax in world configurator Co-authored-by: Copilot <copilot@github.com> * fix biome errors * fix: correct typos and improve error messages in export, load, save, and versioning modules Co-authored-by: Copilot <copilot@github.com> * fix: enhance error handling and improve user feedback in export and load functions Co-authored-by: Copilot <copilot@github.com> * fix: remove outdated export information for Foundry VTT in index.html * fix: increase timeout duration for loading maps from URL and improve error message handling Co-authored-by: Copilot <copilot@github.com> --------- Co-authored-by: Copilot <copilot@github.com>
1 parent e0e1d32 commit beb4f52

16 files changed

Lines changed: 248 additions & 155 deletions

public/modules/io/export.js

Lines changed: 119 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,71 +3,103 @@
33

44
async function exportToSvg() {
55
TIME && console.time("exportToSvg");
6-
const url = await getMapURL("svg", {fullMap: true});
7-
const link = document.createElement("a");
8-
link.download = getFileName() + ".svg";
9-
link.href = url;
10-
link.click();
11-
12-
const message = `${link.download} is saved. Open 'Downloads' screen (ctrl + J) to check`;
13-
tip(message, true, "success", 5000);
14-
TIME && console.timeEnd("exportToSvg");
6+
try {
7+
const url = await getMapURL("svg", {fullMap: true});
8+
const link = document.createElement("a");
9+
link.download = getFileName() + ".svg";
10+
link.href = url;
11+
link.click();
12+
13+
const message = `${link.download} is saved. Open 'Downloads' screen (CTRL + J) to check`;
14+
tip(message, true, "success", 5000);
15+
} catch (error) {
16+
ERROR && console.error(error);
17+
tip(`SVG export failed: ${error?.message || "Unknown error"}`, true, "error", 5000);
18+
} finally {
19+
TIME && console.timeEnd("exportToSvg");
20+
}
1521
}
1622

1723
async function exportToPng() {
1824
TIME && console.time("exportToPng");
19-
const url = await getMapURL("png");
20-
21-
const link = document.createElement("a");
22-
const canvas = document.createElement("canvas");
23-
const ctx = canvas.getContext("2d");
24-
canvas.width = svgWidth * pngResolutionInput.value;
25-
canvas.height = svgHeight * pngResolutionInput.value;
26-
const img = new Image();
27-
img.src = url;
28-
29-
img.onload = function () {
30-
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
31-
link.download = getFileName() + ".png";
32-
canvas.toBlob(function (blob) {
33-
link.href = window.URL.createObjectURL(blob);
34-
link.click();
35-
window.setTimeout(function () {
36-
canvas.remove();
37-
window.URL.revokeObjectURL(link.href);
38-
39-
const message = `${link.download} is saved. Open 'Downloads' screen (ctrl + J) to check. You can set image scale in options`;
40-
tip(message, true, "success", 5000);
41-
}, 1000);
25+
try {
26+
const url = await getMapURL("png");
27+
const link = document.createElement("a");
28+
const canvas = document.createElement("canvas");
29+
const ctx = canvas.getContext("2d");
30+
canvas.width = svgWidth * pngResolutionInput.value;
31+
canvas.height = svgHeight * pngResolutionInput.value;
32+
33+
const blob = await new Promise((resolve, reject) => {
34+
const img = new Image();
35+
img.onload = function () {
36+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
37+
canvas.toBlob(blob => {
38+
if (!blob) return reject(new Error("Cannot render PNG image"));
39+
resolve(blob);
40+
}, "image/png");
41+
};
42+
img.onerror = () => reject(new Error("Cannot load map image for PNG export"));
43+
img.src = url;
4244
});
43-
};
4445

45-
TIME && console.timeEnd("exportToPng");
46+
link.download = getFileName() + ".png";
47+
link.href = window.URL.createObjectURL(blob);
48+
link.click();
49+
window.setTimeout(function () {
50+
canvas.remove();
51+
window.URL.revokeObjectURL(link.href);
52+
}, 1000);
53+
54+
const message = `${link.download} is saved. Open 'Downloads' screen (CTRL + J) to check. You can set image scale in options`;
55+
tip(message, true, "success", 5000);
56+
} catch (error) {
57+
ERROR && console.error(error);
58+
tip(`PNG export failed: ${error?.message || "Unknown error"}`, true, "error", 5000);
59+
} finally {
60+
TIME && console.timeEnd("exportToPng");
61+
}
4662
}
4763

4864
async function exportToJpeg() {
4965
TIME && console.time("exportToJpeg");
50-
const url = await getMapURL("png");
66+
try {
67+
const url = await getMapURL("png");
68+
const canvas = document.createElement("canvas");
69+
const ctx = canvas.getContext("2d");
70+
canvas.width = svgWidth * pngResolutionInput.value;
71+
canvas.height = svgHeight * pngResolutionInput.value;
5172

52-
const canvas = document.createElement("canvas");
53-
canvas.width = svgWidth * pngResolutionInput.value;
54-
canvas.height = svgHeight * pngResolutionInput.value;
55-
const img = new Image();
56-
img.src = url;
57-
58-
img.onload = async function () {
59-
canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
6073
const quality = Math.min(rn(1 - pngResolutionInput.value / 20, 2), 0.92);
61-
const URL = await canvas.toDataURL("image/jpeg", quality);
74+
const blob = await new Promise((resolve, reject) => {
75+
const img = new Image();
76+
img.onload = function () {
77+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
78+
canvas.toBlob(
79+
blob => {
80+
if (!blob) return reject(new Error("Cannot render JPEG image"));
81+
resolve(blob);
82+
},
83+
"image/jpeg",
84+
quality
85+
);
86+
};
87+
img.onerror = () => reject(new Error("Cannot load map image for JPEG export"));
88+
img.src = url;
89+
});
90+
6291
const link = document.createElement("a");
6392
link.download = getFileName() + ".jpeg";
64-
link.href = URL;
93+
link.href = window.URL.createObjectURL(blob);
6594
link.click();
6695
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000);
67-
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
68-
};
69-
70-
TIME && console.timeEnd("exportToJpeg");
96+
window.setTimeout(() => window.URL.revokeObjectURL(link.href), 5000);
97+
} catch (error) {
98+
ERROR && console.error(error);
99+
tip(`JPEG export failed: ${error?.message || "Unknown error"}`, true, "error", 5000);
100+
} finally {
101+
TIME && console.timeEnd("exportToJpeg");
102+
}
71103
}
72104

73105
async function exportToPngTiles() {
@@ -132,17 +164,24 @@ async function exportToPngTiles() {
132164
}
133165

134166
status.innerHTML = "Zipping files...";
135-
zip.generateAsync({type: "blob"}).then(blob => {
136-
status.innerHTML = "Downloading the archive...";
137-
const link = document.createElement("a");
138-
link.href = URL.createObjectURL(blob);
139-
link.download = getFileName() + ".zip";
140-
link.click();
141-
link.remove();
142-
143-
status.innerHTML = 'Done. Check .zip file in "Downloads" (crtl + J)';
144-
setTimeout(() => URL.revokeObjectURL(link.href), 5000);
145-
});
167+
zip
168+
.generateAsync({type: "blob"})
169+
.then(blob => {
170+
status.innerHTML = "Downloading the archive...";
171+
const link = document.createElement("a");
172+
link.href = URL.createObjectURL(blob);
173+
link.download = getFileName() + ".zip";
174+
link.click();
175+
link.remove();
176+
177+
status.innerHTML = 'Done. Check .zip file in "Downloads" (CTRL + J)';
178+
setTimeout(() => URL.revokeObjectURL(link.href), 5000);
179+
})
180+
.catch(error => {
181+
ERROR && console.error(error);
182+
status.innerHTML = "Tiles export failed";
183+
tip(`PNG tiles export failed: ${error?.message || "Unknown error"}`, true, "error", 5000);
184+
});
146185

147186
// promisified img.onload
148187
function loadImage(img) {
@@ -583,70 +622,70 @@ function saveGeoJsonZones() {
583622
// Handles multiple disconnected components and holes properly
584623
function getZonePolygonCoordinates(zoneCells) {
585624
const cellsInZone = new Set(zoneCells);
586-
const ofSameType = (cellId) => cellsInZone.has(cellId);
587-
const ofDifferentType = (cellId) => !cellsInZone.has(cellId);
588-
625+
const ofSameType = cellId => cellsInZone.has(cellId);
626+
const ofDifferentType = cellId => !cellsInZone.has(cellId);
627+
589628
const checkedCells = new Set();
590629
const rings = []; // Array of LinearRings (each ring is an array of coordinates)
591-
630+
592631
// Find all boundary components by tracing each connected region
593632
for (const cellId of zoneCells) {
594633
if (checkedCells.has(cellId)) continue;
595-
634+
596635
// Check if this cell is on the boundary (has a neighbor outside the zone)
597636
const neighbors = cells.c[cellId];
598637
const onBorder = neighbors.some(ofDifferentType);
599638
if (!onBorder) continue;
600-
639+
601640
// Check if this is an inner lake (hole) - skip if so
602641
const feature = pack.features[cells.f[cellId]];
603642
if (feature.type === "lake" && feature.shoreline) {
604643
if (feature.shoreline.every(ofSameType)) continue;
605644
}
606-
645+
607646
// Find a starting vertex that's on the boundary
608647
const cellVertices = cells.v[cellId];
609648
let startingVertex = null;
610-
649+
611650
for (const vertexId of cellVertices) {
612651
const vertexCells = vertices.c[vertexId];
613652
if (vertexCells.some(ofDifferentType)) {
614653
startingVertex = vertexId;
615654
break;
616655
}
617656
}
618-
657+
619658
if (startingVertex === null) continue;
620-
659+
621660
// Use connectVertices to trace the boundary (reusing existing logic)
622661
const vertexChain = connectVertices({
623662
vertices,
624663
startingVertex,
625664
ofSameType,
626-
addToChecked: (cellId) => checkedCells.add(cellId),
627-
closeRing: false, // We'll close it manually after converting to coordinates
665+
addToChecked: cellId => checkedCells.add(cellId),
666+
closeRing: false // We'll close it manually after converting to coordinates
628667
});
629-
668+
630669
if (vertexChain.length < 3) continue;
631-
670+
632671
// Convert vertex chain to coordinates
633672
const coordinates = [];
634673
for (const vertexId of vertexChain) {
635674
const [x, y] = vertices.p[vertexId];
636675
coordinates.push(getCoordinates(x, y, 4));
637676
}
638-
677+
639678
// Close the ring (first coordinate = last coordinate)
640679
if (coordinates.length > 0) {
641680
coordinates.push(coordinates[0]);
642681
}
643-
682+
644683
// Only add ring if it has at least 4 positions (minimum for valid LinearRing)
645684
if (coordinates.length >= 4) {
646685
rings.push(coordinates);
647686
}
648687
}
649-
688+
650689
return rings;
651690
}
652691

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

658697
const rings = getZonePolygonCoordinates(zone.cells);
659-
698+
660699
// Skip if no valid rings were generated
661700
if (rings.length === 0) return;
662-
701+
663702
const properties = {
664703
id: zone.i,
665704
name: zone.name,
666705
type: zone.type,
667706
color: zone.color,
668707
cells: zone.cells
669708
};
670-
709+
671710
// If there's only one ring, use Polygon geometry
672711
if (rings.length === 1) {
673712
const feature = {

public/modules/io/load.js

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -71,24 +71,30 @@ function loadMapPrompt(blob) {
7171
}
7272
}
7373

74-
function loadMapFromURL(maplink, random) {
75-
const URL = decodeURIComponent(maplink);
76-
77-
fetch(URL, {method: "GET", mode: "cors"})
78-
.then(response => {
79-
if (response.ok) return response.blob();
80-
throw new Error("Cannot load map from URL");
81-
})
82-
.then(blob => uploadMap(blob))
83-
.catch(error => {
84-
showUploadErrorMessage(error.message, URL, random);
85-
if (random) generateMapOnLoad();
86-
});
74+
async function loadMapFromURL(maplink, random) {
75+
const controller = new AbortController();
76+
const TIMEOUT = 120000; // 120 seconds
77+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
78+
79+
try {
80+
const url = decodeURIComponent(maplink);
81+
const response = await fetch(url, {method: "GET", mode: "cors", signal: controller.signal});
82+
if (!response.ok) throw new Error("Cannot load map from URL");
83+
84+
const blob = await response.blob();
85+
uploadMap(blob);
86+
} catch (error) {
87+
const message = error?.name === "AbortError" ? "Cannot load map from URL: request timed out" : error.message;
88+
showUploadErrorMessage(message, maplink, random);
89+
if (random) generateMapOnLoad();
90+
} finally {
91+
clearTimeout(timeoutId);
92+
}
8793
}
8894

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

747-
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}.
753+
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}.
748754
<p id="errorBox">${parseError(error)}</p>`;
749755

750756
$("#alert").dialog({

0 commit comments

Comments
 (0)