mirror of
https://github.com/louis-e/arnis.git
synced 2026-01-21 12:37:52 -05:00
562 lines
19 KiB
JavaScript
Vendored
562 lines
19 KiB
JavaScript
Vendored
import { licenseText } from './license.js';
|
|
import { fetchLanguage, invalidJSON } from './language.js';
|
|
|
|
let invoke;
|
|
if (window.__TAURI__) {
|
|
invoke = window.__TAURI__.core.invoke;
|
|
} else {
|
|
function dummyFunc() { }
|
|
window.__TAURI__ = { event: { listen: dummyFunc } };
|
|
invoke = dummyFunc;
|
|
}
|
|
|
|
const DEFAULT_LOCALE_PATH = `./locales/en.json`;
|
|
|
|
// Initialize elements and start the demo progress
|
|
window.addEventListener("DOMContentLoaded", async () => {
|
|
registerMessageEvent();
|
|
window.selectWorld = selectWorld;
|
|
window.startGeneration = startGeneration;
|
|
setupProgressListener();
|
|
initSettings();
|
|
initWorldPicker();
|
|
handleBboxInput();
|
|
const localization = await getLocalization();
|
|
await applyLocalization(localization);
|
|
initFooter();
|
|
await checkForUpdates();
|
|
});
|
|
|
|
// Expose language functions to window for use by language-selector.js
|
|
window.fetchLanguage = fetchLanguage;
|
|
window.applyLocalization = applyLocalization;
|
|
window.initFooter = initFooter;
|
|
|
|
/**
|
|
* Fetches and returns localization data based on user's language
|
|
* Falls back to English if requested language is not available
|
|
* @returns {Promise<Object>} The localization JSON object
|
|
*/
|
|
async function getLocalization() {
|
|
// Check if user has a saved language preference
|
|
const savedLanguage = localStorage.getItem('arnis-language');
|
|
|
|
// If there's a saved preference, use it
|
|
if (savedLanguage) {
|
|
return await fetchLanguage(savedLanguage);
|
|
}
|
|
|
|
// Otherwise use the browser's language
|
|
const lang = navigator.language;
|
|
return await fetchLanguage(lang);
|
|
}
|
|
|
|
/**
|
|
* Updates an HTML element with localized text
|
|
* @param {Object} json - Localization data
|
|
* @param {Object} elementObject - Object containing element or selector
|
|
* @param {string} localizedStringKey - Key for the localized string
|
|
*/
|
|
async function localizeElement(json, elementObject, localizedStringKey) {
|
|
const element =
|
|
(!elementObject.element || elementObject.element === "")
|
|
? document.querySelector(elementObject.selector) : elementObject.element;
|
|
const attribute = localizedStringKey.startsWith("placeholder_") ? "placeholder" : "textContent";
|
|
|
|
if (element) {
|
|
if (localizedStringKey in json) {
|
|
element[attribute] = json[localizedStringKey];
|
|
} else {
|
|
// Fallback to default (English) string
|
|
const defaultJson = await fetchLanguage('en');
|
|
element[attribute] = defaultJson[localizedStringKey];
|
|
}
|
|
}
|
|
}
|
|
|
|
async function applyLocalization(localization) {
|
|
const localizationElements = {
|
|
"h2[data-localize='select_location']": "select_location",
|
|
"#bbox-text": "zoom_in_and_choose",
|
|
"h2[data-localize='select_world']": "select_world",
|
|
"span[id='choose_world']": "choose_world",
|
|
"#selected-world": "no_world_selected",
|
|
"#start-button": "start_generation",
|
|
"h2[data-localize='progress']": "progress",
|
|
"h2[data-localize='choose_world_modal_title']": "choose_world_modal_title",
|
|
"button[data-localize='select_existing_world']": "select_existing_world",
|
|
"button[data-localize='generate_new_world']": "generate_new_world",
|
|
"h2[data-localize='customization_settings']": "customization_settings",
|
|
"label[data-localize='world_scale']": "world_scale",
|
|
"label[data-localize='custom_bounding_box']": "custom_bounding_box",
|
|
"label[data-localize='floodfill_timeout']": "floodfill_timeout",
|
|
"label[data-localize='ground_level']": "ground_level",
|
|
"label[data-localize='language']": "language",
|
|
"label[data-localize='terrain']": "terrain",
|
|
"label[data-localize='fillground']": "fillground",
|
|
".footer-link": "footer_text",
|
|
"button[data-localize='license_and_credits']": "license_and_credits",
|
|
"h2[data-localize='license_and_credits']": "license_and_credits",
|
|
|
|
// Placeholder strings
|
|
"input[id='bbox-coords']": "placeholder_bbox",
|
|
"input[id='floodfill-timeout']": "placeholder_floodfill",
|
|
"input[id='ground-level']": "placeholder_ground"
|
|
};
|
|
|
|
for (const selector in localizationElements) {
|
|
localizeElement(localization, { selector: selector }, localizationElements[selector]);
|
|
}
|
|
|
|
// Update error messages
|
|
window.localization = localization;
|
|
}
|
|
|
|
// Function to initialize the footer with the current year and version
|
|
async function initFooter() {
|
|
const currentYear = new Date().getFullYear();
|
|
let version = "x.x.x";
|
|
|
|
try {
|
|
version = await invoke('gui_get_version');
|
|
} catch (error) {
|
|
console.error("Failed to fetch version:", error);
|
|
}
|
|
|
|
const footerElement = document.querySelector(".footer-link");
|
|
if (footerElement) {
|
|
// Get the original text from localization if available, or use the current text
|
|
let footerText = footerElement.textContent;
|
|
|
|
// Check if the text is from localization and contains placeholders
|
|
if (window.localization && window.localization.footer_text) {
|
|
footerText = window.localization.footer_text;
|
|
}
|
|
|
|
// Replace placeholders with actual values
|
|
footerElement.textContent = footerText
|
|
.replace("{year}", currentYear)
|
|
.replace("{version}", version);
|
|
}
|
|
}
|
|
|
|
// Function to check for updates and display a notification if available
|
|
async function checkForUpdates() {
|
|
try {
|
|
const isUpdateAvailable = await invoke('gui_check_for_updates');
|
|
if (isUpdateAvailable) {
|
|
const footer = document.querySelector(".footer");
|
|
const updateMessage = document.createElement("a");
|
|
updateMessage.href = "https://github.com/louis-e/arnis/releases";
|
|
updateMessage.target = "_blank";
|
|
updateMessage.style.color = "#fecc44";
|
|
updateMessage.style.marginTop = "-5px";
|
|
updateMessage.style.fontSize = "0.95em";
|
|
updateMessage.style.display = "block";
|
|
updateMessage.style.textDecoration = "none";
|
|
|
|
localizeElement(window.localization, { element: updateMessage }, "new_version_available");
|
|
footer.style.marginTop = "15px";
|
|
footer.appendChild(updateMessage);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to check for updates: ", error);
|
|
}
|
|
}
|
|
|
|
// Function to register the event listener for bbox updates from iframe
|
|
function registerMessageEvent() {
|
|
window.addEventListener('message', function (event) {
|
|
const bboxText = event.data.bboxText;
|
|
|
|
if (bboxText) {
|
|
console.log("Updated BBOX Coordinates:", bboxText);
|
|
displayBboxInfoText(bboxText);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Function to set up the progress bar listener
|
|
function setupProgressListener() {
|
|
const progressBar = document.getElementById("progress-bar");
|
|
const progressMessage = document.getElementById("progress-message");
|
|
const progressDetail = document.getElementById("progress-detail");
|
|
|
|
window.__TAURI__.event.listen("progress-update", (event) => {
|
|
const { progress, message } = event.payload;
|
|
|
|
if (progress != -1) {
|
|
progressBar.style.width = `${progress}%`;
|
|
progressDetail.textContent = `${Math.round(progress)}%`;
|
|
}
|
|
|
|
if (message != "") {
|
|
progressMessage.textContent = message;
|
|
|
|
if (message.startsWith("Error!")) {
|
|
progressMessage.style.color = "#fa7878";
|
|
generationButtonEnabled = true;
|
|
} else if (message.startsWith("Done!")) {
|
|
progressMessage.style.color = "#7bd864";
|
|
generationButtonEnabled = true;
|
|
} else {
|
|
progressMessage.style.color = "";
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function initSettings() {
|
|
// Settings
|
|
const settingsModal = document.getElementById("settings-modal");
|
|
const slider = document.getElementById("scale-value-slider");
|
|
const sliderValue = document.getElementById("slider-value");
|
|
|
|
// Open settings modal
|
|
function openSettings() {
|
|
settingsModal.style.display = "flex";
|
|
settingsModal.style.justifyContent = "center";
|
|
settingsModal.style.alignItems = "center";
|
|
}
|
|
|
|
// Close settings modal
|
|
function closeSettings() {
|
|
settingsModal.style.display = "none";
|
|
}
|
|
|
|
window.openSettings = openSettings;
|
|
window.closeSettings = closeSettings;
|
|
|
|
// Update slider value display
|
|
slider.addEventListener("input", () => {
|
|
sliderValue.textContent = parseFloat(slider.value).toFixed(2);
|
|
});
|
|
|
|
// Language selector
|
|
const languageSelect = document.getElementById("language-select");
|
|
// Set initial value based on current language
|
|
const currentLang = navigator.language;
|
|
const availableOptions = Array.from(languageSelect.options).map(opt => opt.value);
|
|
|
|
// Try to match the exact language code first
|
|
if (availableOptions.includes(currentLang)) {
|
|
languageSelect.value = currentLang;
|
|
}
|
|
// Try to match just the base language code
|
|
else if (availableOptions.includes(currentLang.split('-')[0])) {
|
|
languageSelect.value = currentLang.split('-')[0];
|
|
}
|
|
// Default to English
|
|
else {
|
|
languageSelect.value = "en";
|
|
}
|
|
|
|
// Handle language change
|
|
languageSelect.addEventListener("change", async () => {
|
|
const selectedLanguage = languageSelect.value;
|
|
|
|
// Store the selected language in localStorage for persistence
|
|
localStorage.setItem('arnis-language', selectedLanguage);
|
|
|
|
// Reload localization with the new language
|
|
const localization = await fetchLanguage(selectedLanguage);
|
|
await applyLocalization(localization);
|
|
});
|
|
|
|
|
|
/// License and Credits
|
|
function openLicense() {
|
|
const licenseModal = document.getElementById("license-modal");
|
|
const licenseContent = document.getElementById("license-content");
|
|
|
|
// Render the license text as HTML
|
|
licenseContent.innerHTML = licenseText;
|
|
|
|
// Show the modal
|
|
licenseModal.style.display = "flex";
|
|
licenseModal.style.justifyContent = "center";
|
|
licenseModal.style.alignItems = "center";
|
|
}
|
|
|
|
function closeLicense() {
|
|
const licenseModal = document.getElementById("license-modal");
|
|
licenseModal.style.display = "none";
|
|
}
|
|
|
|
window.openLicense = openLicense;
|
|
window.closeLicense = closeLicense;
|
|
}
|
|
|
|
function initWorldPicker() {
|
|
// World Picker
|
|
const worldPickerModal = document.getElementById("world-modal");
|
|
|
|
// Open world picker modal
|
|
function openWorldPicker() {
|
|
worldPickerModal.style.display = "flex";
|
|
worldPickerModal.style.justifyContent = "center";
|
|
worldPickerModal.style.alignItems = "center";
|
|
}
|
|
|
|
// Close world picker modal
|
|
function closeWorldPicker() {
|
|
worldPickerModal.style.display = "none";
|
|
}
|
|
|
|
window.openWorldPicker = openWorldPicker;
|
|
window.closeWorldPicker = closeWorldPicker;
|
|
}
|
|
|
|
/**
|
|
* Validates and processes bounding box coordinates input
|
|
* Supports both comma and space-separated formats
|
|
* Updates the map display when valid coordinates are entered
|
|
*/
|
|
function handleBboxInput() {
|
|
const inputBox = document.getElementById("bbox-coords");
|
|
const bboxInfo = document.getElementById("bbox-info");
|
|
|
|
inputBox.addEventListener("input", function () {
|
|
const input = inputBox.value.trim();
|
|
|
|
if (input === "") {
|
|
bboxInfo.textContent = "";
|
|
bboxInfo.style.color = "";
|
|
selectedBBox = "";
|
|
return;
|
|
}
|
|
|
|
// Regular expression to validate bbox input (supports both comma and space-separated formats)
|
|
const bboxPattern = /^(-?\d+(\.\d+)?)[,\s](-?\d+(\.\d+)?)[,\s](-?\d+(\.\d+)?)[,\s](-?\d+(\.\d+)?)$/;
|
|
|
|
if (bboxPattern.test(input)) {
|
|
const matches = input.match(bboxPattern);
|
|
|
|
// Extract coordinates (Lat / Lng order expected)
|
|
const lat1 = parseFloat(matches[1]);
|
|
const lng1 = parseFloat(matches[3]);
|
|
const lat2 = parseFloat(matches[5]);
|
|
const lng2 = parseFloat(matches[7]);
|
|
|
|
// Validate latitude and longitude ranges in the expected Lat / Lng order
|
|
if (
|
|
lat1 >= -90 && lat1 <= 90 &&
|
|
lng1 >= -180 && lng1 <= 180 &&
|
|
lat2 >= -90 && lat2 <= 90 &&
|
|
lng2 >= -180 && lng2 <= 180
|
|
) {
|
|
// Input is valid; trigger the event with consistent comma-separated format
|
|
const bboxText = `${lat1},${lng1},${lat2},${lng2}`;
|
|
window.dispatchEvent(new MessageEvent('message', { data: { bboxText } }));
|
|
|
|
// Show custom bbox on the map
|
|
let map_container = document.querySelector('.map-container');
|
|
map_container.setAttribute('src', `maps.html#${lat1},${lng1},${lat2},${lng2}`);
|
|
map_container.contentWindow.location.reload();
|
|
|
|
// Update the info text
|
|
localizeElement(window.localization, { element: bboxInfo }, "custom_selection_confirmed");
|
|
bboxInfo.style.color = "#7bd864";
|
|
} else {
|
|
// Valid numbers but invalid order or range
|
|
localizeElement(window.localization, { element: bboxInfo }, "error_coordinates_out_of_range");
|
|
bboxInfo.style.color = "#fecc44";
|
|
selectedBBox = "";
|
|
}
|
|
} else {
|
|
// Input doesn't match the required format
|
|
localizeElement(window.localization, { element: bboxInfo }, "invalid_format");
|
|
bboxInfo.style.color = "#fecc44";
|
|
selectedBBox = "";
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calculates the approximate area of a bounding box in square meters
|
|
* Uses the Haversine formula for geodesic calculations
|
|
* @param {number} lng1 - First longitude coordinate
|
|
* @param {number} lat1 - First latitude coordinate
|
|
* @param {number} lng2 - Second longitude coordinate
|
|
* @param {number} lat2 - Second latitude coordinate
|
|
* @returns {number} Area in square meters
|
|
*/
|
|
function calculateBBoxSize(lng1, lat1, lng2, lat2) {
|
|
// Approximate distance calculation using Haversine formula or geodesic formula
|
|
const toRad = (angle) => (angle * Math.PI) / 180;
|
|
const R = 6371000; // Earth radius in meters
|
|
|
|
const latDistance = toRad(lat2 - lat1);
|
|
const lngDistance = toRad(lng2 - lng1);
|
|
|
|
const a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2) +
|
|
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
|
Math.sin(lngDistance / 2) * Math.sin(lngDistance / 2);
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
|
// Width and height of the box
|
|
const height = R * latDistance;
|
|
const width = R * lngDistance;
|
|
|
|
return Math.abs(width * height);
|
|
}
|
|
|
|
/**
|
|
* Normalizes a longitude value to the range [-180, 180]
|
|
* @param {number} lon - Longitude value to normalize
|
|
* @returns {number} Normalized longitude value
|
|
*/
|
|
function normalizeLongitude(lon) {
|
|
return ((lon + 180) % 360 + 360) % 360 - 180;
|
|
}
|
|
|
|
const threshold1 = 30000000.00;
|
|
const threshold2 = 45000000.00;
|
|
let selectedBBox = "";
|
|
|
|
// Function to handle incoming bbox data
|
|
function displayBboxInfoText(bboxText) {
|
|
let [lng1, lat1, lng2, lat2] = bboxText.split(" ").map(Number);
|
|
|
|
// Normalize longitudes
|
|
lat1 = parseFloat(normalizeLongitude(lat1).toFixed(6));
|
|
lat2 = parseFloat(normalizeLongitude(lat2).toFixed(6));
|
|
selectedBBox = `${lng1} ${lat1} ${lng2} ${lat2}`;
|
|
|
|
const bboxInfo = document.getElementById("bbox-info");
|
|
|
|
// Reset the info text if the bbox is 0,0,0,0
|
|
if (lng1 === 0 && lat1 === 0 && lng2 === 0 && lat2 === 0) {
|
|
bboxInfo.textContent = "";
|
|
selectedBBox = "";
|
|
return;
|
|
}
|
|
|
|
// Calculate the size of the selected bbox
|
|
const selectedSize = calculateBBoxSize(lng1, lat1, lng2, lat2);
|
|
|
|
if (selectedSize > threshold2) {
|
|
localizeElement(window.localization, { element: bboxInfo }, "area_too_large");
|
|
bboxInfo.style.color = "#fa7878";
|
|
} else if (selectedSize > threshold1) {
|
|
localizeElement(window.localization, { element: bboxInfo }, "area_extensive");
|
|
bboxInfo.style.color = "#fecc44";
|
|
} else {
|
|
localizeElement(window.localization, { element: bboxInfo }, "selection_confirmed");
|
|
bboxInfo.style.color = "#7bd864";
|
|
}
|
|
}
|
|
|
|
let worldPath = "";
|
|
let isNewWorld = false;
|
|
|
|
async function selectWorld(generate_new_world) {
|
|
try {
|
|
const worldName = await invoke('gui_select_world', { generateNew: generate_new_world });
|
|
if (worldName) {
|
|
worldPath = worldName;
|
|
isNewWorld = generate_new_world;
|
|
const lastSegment = worldName.split(/[\\/]/).pop();
|
|
document.getElementById('selected-world').textContent = lastSegment;
|
|
document.getElementById('selected-world').style.color = "#fecc44";
|
|
}
|
|
} catch (error) {
|
|
handleWorldSelectionError(error);
|
|
}
|
|
|
|
closeWorldPicker();
|
|
}
|
|
|
|
/**
|
|
* Handles world selection errors and displays appropriate messages
|
|
* @param {number} errorCode - Error code from the backend
|
|
*/
|
|
function handleWorldSelectionError(errorCode) {
|
|
const errorKeys = {
|
|
1: "minecraft_directory_not_found",
|
|
2: "world_in_use",
|
|
3: "failed_to_create_world",
|
|
4: "no_world_selected_error"
|
|
};
|
|
|
|
const errorKey = errorKeys[errorCode] || "unknown_error";
|
|
const selectedWorld = document.getElementById('selected-world');
|
|
localizeElement(window.localization, { element: selectedWorld }, errorKey);
|
|
selectedWorld.style.color = "#fa7878";
|
|
worldPath = "";
|
|
console.error(errorCode);
|
|
}
|
|
|
|
let generationButtonEnabled = true;
|
|
/**
|
|
* Initiates the world generation process
|
|
* Validates required inputs and sends generation parameters to the backend
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function startGeneration() {
|
|
try {
|
|
if (generationButtonEnabled === false) {
|
|
return;
|
|
}
|
|
|
|
if (!selectedBBox || selectedBBox == "0.000000 0.000000 0.000000 0.000000") {
|
|
const bboxInfo = document.getElementById('bbox-info');
|
|
localizeElement(window.localization, { element: bboxInfo }, "select_location_first");
|
|
bboxInfo.style.color = "#fa7878";
|
|
return;
|
|
}
|
|
|
|
if (!worldPath || worldPath === "") {
|
|
const selectedWorld = document.getElementById('selected-world');
|
|
localizeElement(window.localization, { element: selectedWorld }, "select_minecraft_world_first");
|
|
selectedWorld.style.color = "#fa7878";
|
|
return;
|
|
}
|
|
|
|
// Get the map iframe reference
|
|
const mapFrame = document.querySelector('.map-container');
|
|
// Get spawn point coordinates if marker exists
|
|
let spawnPoint = null;
|
|
if (mapFrame && mapFrame.contentWindow && mapFrame.contentWindow.getSpawnPointCoords) {
|
|
const coords = mapFrame.contentWindow.getSpawnPointCoords();
|
|
// Convert object format to tuple format if coordinates exist
|
|
if (coords) {
|
|
spawnPoint = [coords.lat, coords.lng];
|
|
}
|
|
}
|
|
|
|
var terrain = document.getElementById("terrain-toggle").checked;
|
|
var interior = document.getElementById("interior-toggle").checked;
|
|
var roof = document.getElementById("roof-toggle").checked;
|
|
var fill_ground = document.getElementById("fillground-toggle").checked;
|
|
var scale = parseFloat(document.getElementById("scale-value-slider").value);
|
|
var floodfill_timeout = parseInt(document.getElementById("floodfill-timeout").value, 10);
|
|
var ground_level = parseInt(document.getElementById("ground-level").value, 10);
|
|
|
|
// Validate floodfill_timeout and ground_level
|
|
floodfill_timeout = isNaN(floodfill_timeout) || floodfill_timeout < 0 ? 20 : floodfill_timeout;
|
|
ground_level = isNaN(ground_level) || ground_level < -62 ? 20 : ground_level;
|
|
|
|
// Pass the selected options to the Rust backend
|
|
await invoke("gui_start_generation", {
|
|
bboxText: selectedBBox,
|
|
selectedWorld: worldPath,
|
|
worldScale: scale,
|
|
groundLevel: ground_level,
|
|
floodfillTimeout: floodfill_timeout,
|
|
terrainEnabled: terrain,
|
|
interiorEnabled: interior,
|
|
roofEnabled: roof,
|
|
fillgroundEnabled: fill_ground,
|
|
isNewWorld: isNewWorld,
|
|
spawnPoint: spawnPoint
|
|
});
|
|
|
|
console.log("Generation process started.");
|
|
generationButtonEnabled = false;
|
|
} catch (error) {
|
|
console.error("Error starting generation:", error);
|
|
generationButtonEnabled = true;
|
|
}
|
|
}
|