You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

405 lines
16 KiB
JavaScript

/* Copyright (C) 2024 Sebastián Santisi <ssantisi@fi.uba.ar>, CSC-CONICET */
import * as THREE from 'three';
import { STLLoader } from 'https://cdn.jsdelivr.net/npm/three@0.136.0/examples/jsm/loaders/STLLoader.js';
import { WindMill } from './windmill.js';
import { Screen } from './screen.js';
import { ArgentinianFlag } from './argentinianflag.js';
import { checkBoard } from './checkboard.js';
window.three = THREE;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({antialias: true});
document.body.appendChild(renderer.domElement);
//renderer.setPixelRatio( window.devicePixelRatio );
const EXCLAMATION = '⚠️';
const PARTY = '🥳';
class WindSimulation {
constructor(width, height, mills) {
this.width = width;
this.height = height;
this.wms = [];
this.init_windmills(mills);
this.wm = null;
this.simulation = null;
this.simulationPots = null;
this.simulationPot = null;
this.xhr = null;
this.lastFrame = Date.now();
this.rotation = Math.PI;
this.targetRotation = Math.PI;
this.dir = 0;
this.checkBoard = checkBoard(width, height);
scene.add(this.checkBoard);
const loader = new STLLoader();
loader.load('assets/MonumentoBandera.stl', (geometry) => {
var material = new THREE.MeshStandardMaterial({color: 0xffffff});
var mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
var scale = 0.0093;
mesh.scale.setScalar(scale);
mesh.rotation.z = -Math.PI / 2;
mesh.position.x = 9.5;
mesh.position.y = 0.3;
scene.add(mesh);
this.monument = mesh;
});
this.flag = new ArgentinianFlag(0.3);
this.flag.position.set(9.5, 0.3, 0.3);
this.flag.rotation.z = Math.PI;
scene.add(this.flag);
}
init_windmills(mills) {
if(!WindMill.isReady()) {
setTimeout(() => { this.init_windmills(mills) }, 100);
return;
}
for(var i = 0; i < mills; i++)
this.addWindMill();
this.wm = null;
}
move(pos, ended=false, count=2) {
if(this.simulation)
this.resetSimulation();
if(! count)
return false;
var wm = this.wm;
if(ended) {
if(this.wm != null) this.wm.select(false);
this.wm = null;
}
if(wm == null) return false;
if(pos.x > this.width - 0.5 || pos.x < 0.5 || pos.y > this.height - 0.5 || pos.y < 0.5) {
if(pos.x > this.width - 0.5)
pos.x = this.width - 0.5;
if(pos.x < 0.5)
pos.x = 0.5;
if(pos.y > this.height - 0.5)
pos.y = this.height - 0.5;
if(pos.y < 0.5)
pos.y = 0.5;
return this.move(pos, ended, count - 1);
}
for(var i = 0; i < this.wms.length; i++) {
if(this.wms[i] == wm) continue;
if(pos.clone().sub(this.wms[i].position).length() < 1) {
return this.move(this.wms[i].position.clone().add(pos.clone().sub(this.wms[i].position).normalize()), ended, count - 1);
}
}
wm.position.x = pos.x;
wm.position.y = pos.y;
return true;
}
animate() {
requestAnimationFrame(() => { this.animate(); });
var now = Date.now();
var step = (now - this.lastFrame) / 1000;
if(!Screen.isMobile || (Screen.isMobile && screen.clicked)) {
const intersects = screen.raycaster.intersectObjects(this.wms);
var wm = null;
for(var i = 0; i < intersects.length; i++) {
if(intersects[i].object.constructor.name != "Spring") {
wm = intersects[i].object.parent;
break;
}
}
if(!screen.clicked || Screen.isMobile) {
if(this.wm != wm) {
if(this.wm != null) {
this.wm.select(false);
document.getElementById("follower").style.display = "none";
}
if(wm != null) {
wm.select(true);
if(this.simulation) {
var i = this.wms.indexOf(wm);
var follower = document.getElementById("follower");
var perc = Math.round(this.simulationPot[i] * 100 / 14.95);
follower.innerHTML = "<h3>Aerogenerador Nº" + (i + 1) + "</h4>" + (Math.round(this.simulationPot[i] * 100) / 100) + ' GWh<br />' + this.divPercentBar(perc, 50) + '<br />' + perc + '% ' + (perc < 50 ? '⚠️' : '');
follower.style.display = "block";
}
}
}
this.wm = wm;
}
}
for(var i = 0; i < this.wms.length; i++)
this.wms[i].animate(step);
var speed = 2 * step;
if(Math.abs(this.targetRotation - this.rotation) > speed) {
var step = speed * Math.sign(this.targetRotation - this.rotation);
this.rotation += step;
for(var i = 0; i < this.wms.length; i++) {
this.wms[i].rotation.z += step;
}
this.flag.rotation.z += step;
}
this.flag.animate();
if(window.exclamation)
window.exclamation.rotation.y += 0.02;
this.lastFrame = now;
renderer.render(scene, camera);
}
resetSimulation() {
scene.remove(this.simulation);
this.simulation = null;
this.simulationPots = null;
this.simulationPot = null;
for(var i = 0; i < this.wms.length; i++)
this.wms[i].lowPower(false);
document.getElementById("follower").style.display = "none";
document.getElementById("results").style.display = "none";
document.getElementById("numbers").innerHTML = "";
document.getElementById("button_validate").style.display = "none";
}
setRotation(dir) {
if(this.simulation)
this.resetSimulation();
var rotations = {};
rotations[0] = Math.PI;
rotations[270] = Math.PI / 2;
rotations[270 + 45] = Math.PI * 3 / 4;
rotations[270 - 45] = Math.PI / 4;
if(! (dir in rotations)) return false;
this.dir = dir;
this.targetRotation = rotations[dir];
}
positionValid(pos) {
for(let i = 0; i < this.wms.length; i++)
if(pos.clone().sub(this.wms[i].position).length() < 1)
return false;
return true;
}
addWindMill() {
if(this.wms.length >= 10) return;
for(let x = 0; x < 5; x++)
for(let y = 0; y < 10; y++) {
var pos = new THREE.Vector3(4.5 + Math.ceil(x / 2) * (x % 2 ? 1 : -1), 4.5 + Math.ceil(y / 2) * (y % 2 ? 1 : -1), 0);
if(this.positionValid(pos)) {
var wm = new WindMill();
wm.position.set(pos.x, pos.y, 0);
wm.rotation.z = this.targetRotation;
scene.add(wm);
this.wms.push(wm);
return;
}
}
}
removeWindMill() {
if(this.wms.length <= 1) return;
if(this.wms[this.wms.length - 1] == this.wm) this.wm = null;
scene.remove(this.wms.pop());
}
simulate() {
if(this.xhr != null)
this.xhr.abort();
var xhr = new XMLHttpRequest();
this.xhr = xhr;
xhr.open("POST", "/solver/", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = () => {
if(this.xhr != xhr) return;
if (xhr.readyState === 4 && xhr.status === 200) {
var json = JSON.parse(xhr.responseText);
this.add_simulation(json.ws, json.pot);
this.xhr = null;
}
};
var pos = [];
for(var i = 0; i < this.wms.length; i++)
pos.push([this.wms[i].position.y, this.wms[i].position.x]);
var data = JSON.stringify({"pos": pos, "dir": this.dir});
xhr.send(data);
}
validate() {
if(this.simulationPots == null) return;
var html = '<p>En un parque real tenemos que garantizar que la posición de los aerogeneradores sea la adecuada para todos los vientos posibles.</p><p>Este es el resultado de ensayar tu configuración con los 4 vientos disponibles en este simulador:</p><center><table><tr><th>Aerogenerador</th><th>Viento <div class="b">⬅</div></th><th>Viento <div class="b">⬉</div></th><th>Viento <div class="b">⬆</div></th><th>Viento <div class="b">⬈</div></th></tr>';
var pots = {0: 0, 225: 0, 270: 0, 315: 0}
for(let i = 0; i < this.simulationPot.length; i++) {
html += '<tr><td><b>Nº' + (i + 1) + '</b></td>';
[0, 315, 270, 225].forEach(pot => {
var val = this.simulationPots[pot][i];
pots[pot] += val;
var perc = val / 14.95 * 100;
html += '<td>' + this.divPercentBar(perc, 50) + ' ' + Math.round(perc) + '%</td>';
});
html += "</tr>";
}
html += '<tr class="last"><td><b>TOTAL</b></td>';
var notValid = [];
[0, 315, 270, 225].forEach(pot => {
var perc = pots[pot] / 14.95 * 100 / this.simulationPot.length;
if(perc < 80) notValid.push(pot);
html += '<td>' + this.divPercentBar(perc, 80) + ' ' + Math.round(perc) + '%</td>';
});
html += "</table></center>";
if(notValid.length) {
html += '<div style="float: left; font-size: 3em; padding-right: .3em;">' + EXCLAMATION + '</div><p>Para ser una configuración correcta hay que superar el 80% de producción para todas las direcciones de vientos.</p><p>Tu simulación no valida con ' + (notValid.length > 1 ? 'las direcciones ' : 'la dirección ');
var dirs = {0: '⬅', 315: '⬉', 270: '⬆', 225: '⬈'};
for(let i = 0; i < notValid.length; i++) {
html += '<span style="display: inline-block;" class="b">' + dirs[notValid[i]] + '</span>';
}
html += '.</p><p>Usá los botones de arriba a la derecha para simular tu parque con cada una de las direcciones asegurándote de cumplir con el objetivo.</p>';
}
else {
html += '<div style="float: left; font-size: 3em; padding-right: .3em;">' + PARTY + '</div><p>¡Felicitaciones! Tu configuración valida una producción de al menos el 80% para todas las direcciones de vientos del simulador.</p>';
if(this.simulationPot.length < 10)
html += '<p>Utilizá el botón <span class="b">+</span> para agregar más aerogeneradores y producir más energía en tu parque.</p>';
else
html += '<p>¡SAPE! Lograste hacer funcionar tu parque con el máximo de aerogeneradores que permite este simulador.</p>';
}
window.show(html);
}
divPercentBar(perc, limit) {
return '<div class="bar" style="background: linear-gradient(to right, ' + (perc >= limit ? '#fff' : '#f00') + ' ' + perc + '%, #0000 ' + perc + '%);"></div>';
}
add_simulation(ws, pots) {
var pot = pots[this.dir];
var sum = 0;
for(let i = 0; i < pot.length; i++) {
this.wms[i].lowPower(pot[i] < 14.95 * 0.5);
sum += pot[i];
}
var pasa = Math.round(sum / 14.95 / pot.length * 100) >= 80;
var results = document.getElementById("results")
var innerHTML = '<h3>Resultados de la simulación</h3>' + (pasa ?
'<p style="max-width: 16em">' + PARTY + PARTY + PARTY + ' ¡Tu simulación <b>está bien</b> con este viento! Clickeá "¡validar!" para evaluar en los demás.</p>' :
'<p style="max-width: 16em">' + EXCLAMATION + EXCLAMATION + EXCLAMATION + ' ¡Tu simulación <b>no</b> funciona! Probá otras configuraciones.</p>') + '<table>';
for(let i = 0; i < pot.length; i++) {
var perc = Math.round(pot[i] / 14.95 * 100);
innerHTML += '<tr><td><b>Nº' + (i + 1) + ':</b></td><td>' + Math.round(pot[i] * 100) / 100 + ' GWh</td><td>' + this.divPercentBar(perc, 50) + ' ' + perc + '% ' + (perc < 50 ? EXCLAMATION : '') + '</td></tr>'
}
var perc = Math.round(sum / 14.95 / pot.length * 100);
innerHTML += '<tr class="last"><td>TOTAL:</td><td>' + Math.round(sum * 100) / 100 + ' GWh</td><td>' + this.divPercentBar(perc, 80) + ' ' + perc + '% ' + (perc < 80 ? EXCLAMATION : '') + '</td></tr></table>';
results.innerHTML = innerHTML;
results.style.display = "block";
if(Screen.isMobile) {
var numbers = "";
for(let i = 0; i < pot.length; i++) {
var p = new THREE.Vector3(this.wms[i].position.x, this.wms[i].position.y, 1);
p.project(screen.camera);
p.x = (p.x + 1) / 2 * window.innerWidth;
p.y = -(p.y - 1) / 2 * window.innerHeight;
numbers += '<div style="left: ' + p.x + 'px; top: ' + p.y + 'px;">' + (i + 1) + '</div>';
}
document.getElementById("numbers").innerHTML = numbers;
}
var geometry = new THREE.PlaneGeometry(this.width, this.height, 10 * this.width, 10 * this.height);
var colors = new three.BufferAttribute(new Float32Array(geometry.attributes.position.count * 4), 4);
geometry.setAttribute('color', colors);
const material = new THREE.MeshBasicMaterial({vertexColors: true, transparent: true,});
var mesh = new THREE.Mesh(geometry, material);
mesh.position.set(this.width / 2, this.height / 2, 90/126);
for(var i = 0; i < colors.count; i++) {
var j = Math.trunc(i / 101);
colors.setXYZW(i, 1 - ws[i % 101][100 - j] / 8, 0, 0, 1 - ws[i % 101][100 - j] / 8);
}
if(this.simulation)
this.resetSimulation();
document.getElementById("button_validate").style.display = "block";
this.simulation = mesh;
this.simulationPots = pots;
this.simulationPot = pot;
scene.add(mesh);
}
}
var width = 10;
var height = 10;
var ws = new WindSimulation(width, height, 5);
var screen = new Screen(camera, renderer, new THREE.Vector3(width / 2 + 0.5, height / 2, -0.3));
screen.setMoveCallBack((pos, end) => { ws.move(pos, end); });
screen.onResize();
/*const light = new THREE.DirectionalLight("#FFFFFF");
light.position.set(5,5,100);
//scene.add(light);*/
const ambientLight = new THREE.AmbientLight("#999999", 0.6);
scene.add(ambientLight);
const light = new THREE.DirectionalLight( 0xfff8ee, 1 );
light.position.set(6, -10, 10); //default; light shining from top
//light.target.position.set(5, 5, 0);
light.castShadow = true; // default false
scene.add( light );
light.shadow.mapSize.width = 1024; // default 512
light.shadow.mapSize.height = 1024; // default 512
light.shadow.camera.near = 5;
light.shadow.camera.far = 25;
light.shadow.camera.right = 3.5 + 5;
light.shadow.camera.left = 3.5 - 5;
light.shadow.camera.top = 6 + 6;
light.shadow.camera.bottom = 6 - 6;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
window.light = light;
//const helper = new THREE.CameraHelper( light.shadow.camera );
//scene.add( helper);
//window.helper = helper;
window.ws = ws;
window.screen = screen;
window.scene = scene;
ws.animate();