diff --git a/index.html b/index.html index 0cf8fece..488c7f2c 100644 --- a/index.html +++ b/index.html @@ -2352,13 +2352,8 @@ @@ -2373,7 +2368,7 @@ min="-50" max="50" /> - °C = °F + °C = - °C = °F + °C = - - - - - - - + + + + + + + diff --git a/main.js b/main.js index ebf156bb..9f7cbcea 100644 --- a/main.js +++ b/main.js @@ -5,8 +5,8 @@ // set debug options const PRODUCTION = location.hostname && location.hostname !== "localhost" && location.hostname !== "127.0.0.1"; const DEBUG = localStorage.getItem("debug"); -const INFO = DEBUG || !PRODUCTION; -const TIME = DEBUG || !PRODUCTION; +const INFO = true; +const TIME = true; const WARN = true; const ERROR = true; @@ -179,15 +179,17 @@ function onZoom() { const onZoomDebouced = debounce(onZoom, 50); const zoom = d3.zoom().scaleExtent([1, 20]).on("zoom", onZoomDebouced); -// default options +// default options, based on Earth data let options = { pinNotes: false, showMFCGMap: true, winds: [225, 45, 225, 315, 135, 315], - tempNorthPole: 0, - tempSouthPole: 0, + temperatureEquator: 27, + temperatureNorthPole: -30, + temperatureSouthPole: -15, stateLabelsMode: "auto" }; + let mapCoordinates = {}; // map coordinates on globe let populationRate = +document.getElementById("populationRateInput").value; let distanceScale = +document.getElementById("distanceScaleInput").value; @@ -469,7 +471,7 @@ function applyDefaultBiomesSystem() { const biomesMartix = [ // hot ↔ cold [>19°C; <-4°C]; dry ↕ wet new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10]), - new Uint8Array([3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10, 10, 10]), + new Uint8Array([1, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10, 10, 10]), new Uint8Array([5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 10, 10, 10]), new Uint8Array([5, 6, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 10]), new Uint8Array([7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10, 10]) @@ -1010,29 +1012,43 @@ function calculateTemperatures() { TIME && console.time("calculateTemperatures"); const cells = grid.cells; cells.temp = new Int8Array(cells.i.length); // temperature array - const tEq = +temperatureEquatorInput.value; - const tNorthPole = +temperatureNorthPoleInput.value; - const tSouthPole = +temperatureSouthPoleInput.value; - //Update Settings to match the slider(there may be a better solution) - options.tempSouthPole = +tSouthPole; - options.tempNorthPole = +tNorthPole; - const tNDelta = tEq - tNorthPole; - const tSDelta = tEq - tSouthPole; - const int = d3.easePolyInOut.exponent(0.5); // interpolation function - d3.range(0, cells.i.length, grid.cellsX).forEach(function (r) { - const y = grid.points[r][1]; - const lat = (mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT); // [-90; 90] - const initTemp = tEq - (Math.max(rn(lat / 90, 2), 0) * tNDelta - Math.min(rn(lat / 90, 2), 0) * tSDelta); - for (let i = r; i < r + grid.cellsX; i++) { - cells.temp[i] = minmax(initTemp - convertToFriendly(cells.h[i]), -128, 127); + const {temperatureEquator, temperatureNorthPole, temperatureSouthPole} = options; + const tropics = [16, -20]; // tropics zone + const tropicalGradient = 0.15; + + const tempNorthTropic = temperatureEquator - tropics[0] * tropicalGradient; + const northernGradient = (tempNorthTropic - temperatureNorthPole) / (90 - tropics[0]); + + const tempSouthTropic = temperatureEquator + tropics[1] * tropicalGradient; + const southernGradient = (tempSouthTropic - temperatureSouthPole) / (90 + tropics[1]); + + const exponent = +heightExponentInput.value; + + for (let rowCellId = 0; rowCellId < cells.i.length; rowCellId += grid.cellsX) { + const [, y] = grid.points[rowCellId]; + const rowLatitude = mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT; // [90; -90] + const tempSeaLevel = calculateSeaLevelTemp(rowLatitude); + DEBUG && console.info(`${rn(rowLatitude)}° sea temperature: ${rn(tempSeaLevel)}°C`); + + for (let cellId = rowCellId; cellId < rowCellId + grid.cellsX; cellId++) { + const tempAltitudeDrop = getAltitudeTemperatureDrop(cells.h[cellId]); + cells.temp[cellId] = minmax(tempSeaLevel - tempAltitudeDrop, -128, 127); } - }); + } - // temperature decreases by 6.5 degree C per 1km - function convertToFriendly(h) { + function calculateSeaLevelTemp(latitude) { + const isTropical = latitude <= 16 && latitude >= -20; + if (isTropical) return temperatureEquator - Math.abs(latitude) * tropicalGradient; + + return latitude > 0 + ? tempNorthTropic - (latitude - tropics[0]) * northernGradient + : tempSouthTropic + (latitude - tropics[1]) * southernGradient; + } + + // temperature drops by 6.5°C per 1km of altitude + function getAltitudeTemperatureDrop(h) { if (h < 20) return 0; - const exponent = +heightExponentInput.value; const height = Math.pow(h - 18, exponent); return rn((height / 1000) * 6.5); } diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js index 1ba05747..ecb22ac6 100644 --- a/modules/dynamic/auto-update.js +++ b/modules/dynamic/auto-update.js @@ -639,7 +639,7 @@ export function resolveVersionConflicts(version) { if (version < 1.89) { //May need a major bump - options.tempNorthPole = +temperatureNorthPoleInput.value; - options.tempSouthPole = +temperatureNorthPoleInput.value; + options.temperatureNorthPole = +temperatureNorthPoleInput.value; + options.temperatureSouthPole = +temperatureNorthPoleInput.value; } } diff --git a/modules/io/load.js b/modules/io/load.js index e47f27ac..fde69d42 100644 --- a/modules/io/load.js +++ b/modules/io/load.js @@ -589,8 +589,8 @@ async function parseLoadedData(data) { // set options yearInput.value = options.year; eraInput.value = options.era; - temperatureNorthPoleOutput.value = temperatureNorthPoleInput.value = options.tempNorthPole; - temperatureSouthPoleOutput.value = temperatureSouthPoleInput.value = options.tempSouthPole; + temperatureNorthPoleOutput.value = temperatureNorthPoleInput.value = options.temperatureNorthPole; + temperatureSouthPoleOutput.value = temperatureSouthPoleInput.value = options.temperatureSouthPole; shapeRendering.value = viewbox.attr("shape-rendering") || "geometricPrecision"; if (window.restoreDefaultEvents) restoreDefaultEvents(); diff --git a/modules/ui/options.js b/modules/ui/options.js index 44f8fdd4..04d65494 100644 --- a/modules/ui/options.js +++ b/modules/ui/options.js @@ -559,11 +559,10 @@ function applyStoredOptions() { if (key.slice(0, 5) === "style") applyOption(stylePreset, key, key.slice(5)); } - if (stored("winds")) - options.winds = localStorage - .getItem("winds") - .split(",") - .map(w => +w); + if (stored("winds")) options.winds = localStorage.getItem("winds").split(",").map(Number); + if (stored("temperatureEquator")) options.temperatureEquator = +localStorage.getItem("temperatureEquator"); + if (stored("temperatureNorthPole")) options.temperatureNorthPole = +localStorage.getItem("temperatureNorthPole"); + if (stored("temperatureSouthPole")) options.temperatureSouthPole = +localStorage.getItem("temperatureSouthPole"); if (stored("military")) options.military = JSON.parse(stored("military")); if (stored("tooltipSize")) changeTooltipSize(stored("tooltipSize")); @@ -607,15 +606,10 @@ function randomizeOptions() { if (randomize || !locked("culturesSet")) randomizeCultureSet(); // 'Configure World' settings + if (randomize || !locked("temperatureEquator")) options.temperatureEquator = gauss(25, 7, 20, 35, 0); + if (randomize || !locked("temperatureNorthPole")) options.temperatureNorthPole = gauss(-25, 7, -40, 10, 0); + if (randomize || !locked("temperatureSouthPole")) options.temperatureSouthPole = gauss(-15, 7, -40, 10, 0); if (randomize || !locked("prec")) precInput.value = precOutput.value = gauss(100, 40, 5, 500); - const tMax = 30, - tMin = -30; // temperature extremes - if (randomize || !locked("temperatureEquator")) - temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax - 10, tMax); - if (randomize || !locked("temperatureNorthPole")) - temperatureNorthPoleOutput.value = temperatureNorthPoleInput.value = rand(tMin, tMin + 30); - if (randomize || !locked("temperatureSouthPole")) - temperatureSouthPoleOutput.value = temperatureSouthPoleInput.value = rand(tMin, tMin + 30); // 'Units Editor' settings const US = navigator.language === "en-US"; @@ -1074,8 +1068,7 @@ function toggle3dOptions() { document.getElementById("options3dGlobeResolution").addEventListener("change", changeResolution); // document.getElementById("options3dMeshWireframeMode").addEventListener("change",toggleWireframe3d); document.getElementById("options3dSunColor").addEventListener("input", changeSunColor); - document.getElementById("options3dSubdivide").addEventListener("change",toggle3dSubdivision); - + document.getElementById("options3dSubdivide").addEventListener("change", toggle3dSubdivision); function updateValues() { const globe = document.getElementById("canvas3d").dataset.type === "viewGlobe"; @@ -1115,7 +1108,7 @@ function toggle3dOptions() { ThreeD.setLightness(this.value / 100); } - function changeSunColor(){ + function changeSunColor() { ThreeD.setSunColor(options3dSunColor.value); } @@ -1136,7 +1129,7 @@ function toggle3dOptions() { ThreeD.toggleLabels(); } - function toggle3dSubdivision(){ + function toggle3dSubdivision() { ThreeD.toggle3dSubdivision(); } diff --git a/modules/ui/world-configurator.js b/modules/ui/world-configurator.js index 635df09c..9005c473 100644 --- a/modules/ui/world-configurator.js +++ b/modules/ui/world-configurator.js @@ -1,5 +1,6 @@ function editWorld() { if (customization) return; + $("#worldConfigurator").dialog({ title: "Configure World", resizable: false, @@ -25,28 +26,54 @@ function editWorld() { }); const globe = d3.select("#globe"); - const clr = d3.scaleSequential(d3.interpolateSpectral); - const tMax = 30, - tMin = -25; // temperature extremes const projection = d3.geoOrthographic().translate([100, 100]).scale(100); const path = d3.geoPath(projection); + updateInputValues(); updateGlobeTemperature(); updateGlobePosition(); if (modules.editWorld) return; modules.editWorld = true; - document.getElementById("worldControls").addEventListener("input", e => updateWorld(e.target)); + byId("worldControls").addEventListener("input", e => updateWorld(e.target)); globe.select("#globeWindArrows").on("click", changeWind); globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule updateWindDirections(); + function updateInputValues() { + byId("temperatureEquatorInput").value = options.temperatureEquator; + byId("temperatureEquatorOutput").value = options.temperatureEquator; + byId("temperatureEquatorF").innerText = convertTemperature(options.temperatureEquator, "°F"); + + byId("temperatureNorthPoleInput").value = options.temperatureNorthPole; + byId("temperatureNorthPoleOutput").value = options.temperatureNorthPole; + byId("temperatureNorthPoleF").innerText = convertTemperature(options.temperatureNorthPole, "°F"); + + byId("temperatureSouthPoleInput").value = options.temperatureSouthPole; + byId("temperatureSouthPoleOutput").value = options.temperatureSouthPole; + byId("temperatureSouthPoleF").innerText = convertTemperature(options.temperatureSouthPole, "°F"); + } + function updateWorld(el) { - if (el) { - document.getElementById(el.dataset.stored + "Input").value = el.value; - document.getElementById(el.dataset.stored + "Output").value = el.value; - if (el.dataset.stored) lock(el.dataset.stored); + if (el?.dataset.stored) { + const stored = el.dataset.stored; + byId(stored + "Input").value = el.value; + byId(stored + "Output").value = el.value; + lock(el.dataset.stored); + + if (stored === "temperatureEquator") { + options.temperatureEquator = Number(el.value); + byId("temperatureEquatorF").innerText = convertTemperature(options.temperatureEquator, "°F"); + } + if (stored === "temperatureNorthPole") { + options.temperatureNorthPole = Number(el.value); + byId("temperatureNorthPoleF").innerText = convertTemperature(options.temperatureNorthPole, "°F"); + } + if (stored === "temperatureSouthPole") { + options.temperatureSouthPole = Number(el.value); + byId("temperatureSouthPoleF").innerText = convertTemperature(options.temperatureSouthPole, "°F"); + } } updateGlobeTemperature(); @@ -65,11 +92,11 @@ function editWorld() { if (layerIsOn("toggleBiomes")) drawBiomes(); if (layerIsOn("toggleCoordinates")) drawCoordinates(); if (layerIsOn("toggleRivers")) drawRivers(); - if (document.getElementById("canvas3d")) setTimeout(ThreeD.update(), 500); + if (byId("canvas3d")) setTimeout(ThreeD.update(), 500); } function updateGlobePosition() { - const size = +document.getElementById("mapSizeOutput").value; + const size = +byId("mapSizeOutput").value; const eqD = ((graphHeight / 2) * 100) / size; calculateMapCoordinates(); @@ -77,12 +104,12 @@ function editWorld() { const scale = +distanceScaleInput.value; const unit = distanceUnitInput.value; const meridian = toKilometer(eqD * 2 * scale); - document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`; - document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`; - document.getElementById("meridianLength").innerHTML = rn(eqD * 2); - document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`; - document.getElementById("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : ""; - document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`; + byId("mapSize").innerHTML = `${graphWidth}x${graphHeight}`; + byId("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`; + byId("meridianLength").innerHTML = rn(eqD * 2); + byId("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`; + byId("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : ""; + byId("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`; function toKilometer(v) { if (unit === "km") return v; @@ -104,22 +131,24 @@ function editWorld() { globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area } + // update temperatures on globe (visual-only) function updateGlobeTemperature() { - const tEq = +document.getElementById("temperatureEquatorOutput").value; - document.getElementById("temperatureEquatorF").innerHTML = rn((tEq * 9) / 5 + 32); - const tNorthPole = +document.getElementById("temperatureNorthPoleOutput").value; - document.getElementById("temperatureNorthPoleF").innerHTML = rn((tNorthPole * 9) / 5 + 32); - const tSouthPole = +document.getElementById("temperatureSouthPoleOutput").value; - document.getElementById("temperatureSouthPoleF").innerHTML = rn((tSouthPole * 9) / 5 + 32); + const tEq = options.temperatureEquator; + const tNP = options.temperatureNorthPole; + const tSP = options.temperatureSouthPole; - //North to Equator to South - globe.select(".tempNorthGradient90").attr("stop-color", clr(1 - (tNorthPole - tMin) / (tMax - tMin))); - globe.select(".tempNorthGradient60").attr("stop-color", clr(1 - (tEq - ((tEq - tNorthPole) * 2) / 3 - tMin) / (tMax - tMin))); - globe.select(".tempNorthGradient30").attr("stop-color", clr(1 - (tEq - ((tEq - tNorthPole) * 1) / 3 - tMin) / (tMax - tMin))); - globe.select(".tempGradient0").attr("stop-color", clr(1 - (tEq - tMin) / (tMax - tMin))); - globe.select(".tempSouthGradient30").attr("stop-color", clr(1 - (tEq - ((tEq - tSouthPole) * 1) / 3 - tMin) / (tMax - tMin))); - globe.select(".tempSouthGradient60").attr("stop-color", clr(1 - (tEq - ((tEq - tSouthPole) * 2) / 3 - tMin) / (tMax - tMin))); - globe.select(".tempSouthGradient90").attr("stop-color", clr(1 - (tSouthPole - tMin) / (tMax - tMin))); + const scale = d3.scaleSequential(d3.interpolateSpectral); + const getColor = value => scale(1 - value); + const [tMin, tMax] = [-25, 30]; // temperature extremes + const tDelta = tMax - tMin; + + globe.select("#grad90").attr("stop-color", getColor((tNP - tMin) / tDelta)); + globe.select("#grad60").attr("stop-color", getColor((tEq - ((tEq - tNP) * 2) / 3 - tMin) / tDelta)); + globe.select("#grad30").attr("stop-color", getColor((tEq - ((tEq - tNP) * 1) / 3 - tMin) / tDelta)); + globe.select("#grad0").attr("stop-color", getColor((tEq - tMin) / tDelta)); + globe.select("#grad-30").attr("stop-color", getColor((tEq - ((tEq - tSP) * 1) / 3 - tMin) / tDelta)); + globe.select("#grad-60").attr("stop-color", getColor((tEq - ((tEq - tSP) * 2) / 3 - tMin) / tDelta)); + globe.select("#grad-90").attr("stop-color", getColor((tSP - tMin) / tDelta)); } function updateWindDirections() { @@ -153,8 +182,8 @@ function editWorld() { } function applyWorldPreset(size, lat) { - document.getElementById("mapSizeInput").value = document.getElementById("mapSizeOutput").value = size; - document.getElementById("latitudeInput").value = document.getElementById("latitudeOutput").value = lat; + byId("mapSizeInput").value = byId("mapSizeOutput").value = size; + byId("latitudeInput").value = byId("latitudeOutput").value = lat; lock("mapSize"); lock("latitude"); updateWorld(); diff --git a/utils/unitUtils.js b/utils/unitUtils.js index a9a933df..d940e349 100644 --- a/utils/unitUtils.js +++ b/utils/unitUtils.js @@ -13,8 +13,7 @@ const temperatureConversionMap = { "°Rø": temp => rn((temp * 21) / 40 + 7.5) + "°Rø" }; -function convertTemperature(temp) { - const scale = temperatureScale.value || "°C"; +function convertTemperature(temp, scale = temperatureScale.value || "°C") { return temperatureConversionMap[scale](temp); }