layers_ElevationLayer.js

import * as THREE from 'three';
import { RasterLayer } from './RasterLayer.js';
import { TerrainMeshGenerator } from '../planet/TerrainMeshGenerator';
import {ElevationMesherWorker} from './workers/ElevationMesher.worker.js';

const terrainMeshGenerator = new TerrainMeshGenerator();
let id = 0;
let nextWorker = 0;
function getConcurency() {
    /* if ('hardwareConcurrency' in navigator) {
        return navigator.hardwareConcurrency;
    } else {
        return 4;
    } */
    return 1;
}
const meshGeneratorWorkers = [];
const workerCallbacks = new Map();
const workerOnErrors = new Map();

const blob = new Blob([ElevationMesherWorker.getScript()], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(blob);
for (let i = 0; i < getConcurency(); i++) {
    const elevationMesherWorker = new Worker(workerUrl);
    elevationMesherWorker.onmessage = handleWorkerResponse;
    elevationMesherWorker.onerror = handleWorkerError;
    meshGeneratorWorkers.push(elevationMesherWorker);
}

function sendWorkerTask(data, callback, onerror) {
    const messageID = id++;
    workerCallbacks.set(messageID, callback);
    workerOnErrors.set(messageID, onerror);
    nextWorker = (nextWorker + 1) % meshGeneratorWorkers.length;
    meshGeneratorWorkers[nextWorker].postMessage({ id: messageID, input: data });
}

function handleWorkerResponse(e) {
    if (e.data.error) {
        workerOnErrors.get(e.data.id)(e.data.error);
    } else {
        workerCallbacks.get(e.data.id)(e.data.result);
    }
    workerCallbacks.delete(e.data.id);
    workerOnErrors.delete(e.data.id);
}
function handleWorkerError(error) {
    console.error("uncaught elevation mesher worker error : " + error)
}


/**
 * Base constructor for all terrain elevation layers.
 * @class
 * @extends RasterLayer
 */
class ElevationLayer extends RasterLayer {
    /**
     * Base constructor for elevation layers.
     * @param {Object} properties 
     * @param {String|Number} properties.id layer id should be unique
     * @param {String} properties.name the name can be anything you want and is intended for labeling
     * @param {Number[]} properties.bounds min longitude, min latitude, max longitude, max latitude in degrees
     * @param {Boolean} [properties.visible = true] layer will be rendered if true (true by default)
     */
    constructor(properties) {
        super(properties);
        this.isElevationLayer = true;
    }

    /**
     * Returns a 2D elevation array and populates a tile's geometry and skirtGeometry.
     * The generated geometry does not have to match the elevation array exactly. It can represent overhanging features for example but 
     * the elevation array is expected to correspond at least roughly to the geometry's highest points.
     * 
     * @param {THREE.Box2} bounds 
     * @param {Number} width width resolution for the elevation
     * @param {Number} height height resolution for the elevation
     * @param {THREE.BufferGeometry|undefined} geometry a tile's buffer geometry to be filled with actual geometry
     * @param {THREE.BufferGeometry|undefined} skirtGeometry a skirt geometry to be filled with actual skirts
     * @returns {Promise} a promise for an elevation array
     */
    getElevation(bounds, width, height, geometry, skirtGeometry) {
        throw "not implemented, should be implemented by children of ElevationLayer"
        // to be implemented by children
    }

    /**
     * A default mesh generation function given some elevation.
     * 
     * @param {THREE.Box2} bounds 
     * @param {Number} width 
     * @param {Number} height 
     * @param {Number[]} extendedElevation elevation array extended by 1 in all directions (for correct normals on the edges) 
     * @param {THREE.BufferGeometry} geometry 
     * @param {THREE.BufferGeometry} skirtGeometry 
     * @returns {Promise(THREE.Vector3)} the shift to apply to the tile (for numerical stability)
     */
    _simpleMeshFromElevation(bounds, width, height, extendedElevation, geometry, skirtGeometry) {

        let shift;
        if (bounds.max.y >= 1.57079632) {
            shift = terrainMeshGenerator.generateNorthPoleTile(geometry, skirtGeometry, width, bounds, extendedElevation);
        } else if (bounds.min.y <= -1.57079632) {
            shift = terrainMeshGenerator.generateSouthPoleTile(geometry, skirtGeometry, width, bounds, extendedElevation);
        } else {
            shift = terrainMeshGenerator.generateBaseTile(geometry, skirtGeometry, width, bounds, extendedElevation);
        }
        return shift;
    }

    /**
     * A default mesh generation function given some elevation using web workers.
     * 
     * @param {THREE.Box2} bounds 
     * @param {Number} width 
     * @param {Number} height 
     * @param {Number[]} extendedElevation elevation array extended by 1 in all directions (for correct normals on the edges) 
     * @param {THREE.BufferGeometry} geometry 
     * @param {THREE.BufferGeometry} skirtGeometry 
     * @returns {Promise(THREE.Vector3)} the shift to apply to the tile (for numerical stability)
     */
    _simpleMeshFromElevationAsync(bounds, width, height, extendedElevation, geometry, skirtGeometry) {

        return new Promise((resolve, reject) => {
            sendWorkerTask({ bounds: bounds, resolution: width, extendedElevation: extendedElevation },
                (response) => {
                    //console.log(response);
                    geometry.setIndex(new THREE.Uint32BufferAttribute(new Int32Array(response.indices),1));
                    geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(response.vertices), 3));
                    geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(response.normals), 3));
                    geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(response.uvs), 2));

                    skirtGeometry.setIndex(new THREE.Uint32BufferAttribute(new Int32Array(response.skirtIndices),1));
                    skirtGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(response.skirts), 3));
                    skirtGeometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(response.skirtNormals), 3));
                    skirtGeometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(response.skirtUVs), 2));

                    geometry.computeBoundingSphere();
                    geometry.computeBoundingBox();
                    skirtGeometry.computeBoundingBox();
                    skirtGeometry.computeBoundingSphere();
                    resolve(new THREE.Vector3(response.shift.x, response.shift.y, response.shift.z));
                }, (error) => {
                    reject(error);
                });
        });
    }

    /**
     * Trims the edges of an elevation array by 1 on all sides
     * @param {*} arr elevation array width+2 height+2
     * @param {*} width the desired width
     * @param {*} height the desired height
     * @returns the trimmed array
     */
    _trimEdges(arr, width, height,) {
        const result = [];

        for (let row = 1; row < height - 1; row++) {
            for (let col = 1; col < width - 1; col++) {
                result.push(arr[row * width + col]);
            }
        }

        return result;
    }
}

export { ElevationLayer }