itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
227 lines (215 loc) • 9.29 kB
JavaScript
import * as THREE from 'three';
import GeometryLayer from "./GeometryLayer.js";
import OrientedImageMaterial from "../Renderer/OrientedImageMaterial.js";
import GeoJsonParser from "../Parser/GeoJsonParser.js";
import CameraCalibrationParser from "../Parser/CameraCalibrationParser.js";
import { Coordinates, OrientationUtils } from '@itowns/geographic';
const coord = new Coordinates('EPSG:4978', 0, 0, 0);
const commandCancellation = cmd => cmd.requester.id !== cmd.layer.currentPano.id;
function updatePano(context, camera, layer) {
const newPano = layer.mostNearPano(camera.position);
// detection of oriented image change
const currentId = layer.currentPano ? layer.currentPano.id : undefined;
if (newPano && currentId != newPano.id) {
layer.currentPano = newPano;
// callback to indicate current pano has changed
layer.onPanoChanged({
previousPanoPosition: layer.getPreviousPano() ? layer.getPreviousPano().position : undefined,
currentPanoPosition: layer.getCurrentPano().position,
nextPanoPosition: layer.getNextPano().position
});
// prepare informations about the needed textures
const panoCameras = newPano.geometries[0].properties.idSensors;
const imagesInfo = layer.cameras.map(cam => ({
cameraId: cam.name,
panoId: newPano.id,
as: () => {}
})).filter(info => !panoCameras || panoCameras.includes(info.cameraId));
const command = {
layer,
// put informations about image URL as extent to be used by generic DataSourceProvider, OrientedImageSource will use that.
extentsSource: imagesInfo,
view: context.view,
requester: newPano,
earlyDropFunction: commandCancellation
};
// async call to scheduler to get textures
context.scheduler.execute(command).then(textures => {
if (newPano.id === layer.currentPano.id) {
layer.material.setTextures(textures, newPano, layer.getCamerasNameFromFeature(newPano));
layer.material.updateUniforms(context.camera.camera3D);
context.view.notifyChange(layer, true);
}
}, () => {});
}
}
function updateBackground(layer) {
if (layer.background && layer.currentPano) {
layer.background.position.copy(layer.currentPano.position);
layer.background.updateMatrixWorld();
layer.background.material = layer.material || layer.background.material;
}
}
function createBackground(radius) {
if (!radius || radius <= 0) {
return undefined;
}
const geometry = new THREE.SphereGeometry(radius, 32, 32);
const material = new THREE.MeshPhongMaterial({
color: 0x7777ff,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.5,
wireframe: true
});
const sphere = new THREE.Mesh(geometry, material);
sphere.visible = true;
sphere.name = 'OrientedImageBackground';
return sphere;
}
/**
* OrientedImageLayer loads oriented images, and project these textures on the scene.
* It is design to create an immersive view. </br>
* It loads a set of panoramic position and orientation,
* a set of camera calibration file (it's the same set of camera for each panoramic),
* and a set of texture (each image for each camera for each panoramic), all organised in an {@link OrientedImageSource}. </br>
* It creates an {@link OrientedImageMaterial} used to do projective texture mapping on the scene.
* @extends GeometryLayer
*/
class OrientedImageLayer extends GeometryLayer {
/**
* @param { string } id - The id of the layer, a unique name.
* @param { Object } config - configuration of the layer
* @param { number } config.backgroundDistance - Radius in meter of the sphere used as a background
* @param { function } config.onPanoChanged - callback fired when current panoramic changes
* @param { string } config.crs - crs projection of the view
* @param { string } config.orientation - Json object, using GeoJSon format to represent points,
* it's a set of panoramic position and orientation.
* @param { string } config.calibrations - Json object, representing a set of camera.
* see {@link CameraCalibrationParser}
* @param { OrientedImageSource } config.source - Source used to build url of texture for each oriented image,
* a tecture is need for each camera, for each panoramic.
*/
constructor(id) {
let config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
const {
backgroundDistance,
background = createBackground(backgroundDistance),
onPanoChanged = () => {},
getCamerasNameFromFeature = () => {},
...geometryOptions
} = config;
if (config.projection) {
console.warn('OrientedImageLayer projection parameter is deprecated, use crs instead.');
config.crs = config.crs || config.projection;
}
super(id, new THREE.Group(), geometryOptions);
this.isOrientedImageLayer = true;
this.background = background;
if (this.background) {
// Add layer id to easily identify the objects later on (e.g. to delete the geometries when deleting the layer)
this.background.layer = this.background.layer ?? {};
this.background.layer.id = this.background.layer.id ?? id;
this.object3d.add(this.background);
}
// currentPano is the current point, means it's the closest from the camera
this.currentPano = undefined;
// store a callback to fire event when current panoramic change
this.onPanoChanged = onPanoChanged;
// function to get cameras name from panoramic feature
this.getCamerasNameFromFeature = getCamerasNameFromFeature;
const resolve = this.addInitializationStep();
this.mergeFeatures = false;
this.filteringExtent = false;
this.accurate = true;
const options = {
out: this
};
// panos is an array of feature point, representing many panoramics.
// for each point, there is a position and a quaternion attribute.
this.source.whenReady.then(metadata => GeoJsonParser.parse(config.orientation || metadata.orientation, options).then(orientation => {
this.panos = orientation.features;
// the crs input is parsed in geojson parser
// and returned in options.in
const crsIn = options.in.crs;
const crsOut = config.crs;
const crs2crs = OrientationUtils.quaternionFromCRSToCRS(crsIn, crsOut);
const quat = new THREE.Quaternion();
// add position and quaternion attributes from point feature
let i = 0;
for (const pano of this.panos) {
// set position
coord.crs = pano.crs;
coord.setFromArray(pano.vertices).applyMatrix4(orientation.matrix);
pano.position = coord.toVector3();
// set quaternion
crs2crs(coord, quat);
pano.quaternion = OrientationUtils.quaternionFromAttitude(pano.geometries[0].properties).premultiply(quat);
pano.id = pano.geometries[0].properties.id;
pano.index = i++;
}
}).then(() => {
// array of cameras, represent the projective texture configuration for each panoramic.
CameraCalibrationParser.parse(config.calibration || metadata.calibration, config).then(cameras => {
this.cameras = cameras;
// create the material
this.material = new OrientedImageMaterial(this.cameras, config);
resolve();
});
}));
}
// eslint-disable-next-line
update() {}
set boostLight(value) {
this.material.uniforms.boostLight.value = value;
}
get boostLight() {
return this.material.uniforms.boostLight.value;
}
preUpdate(context) {
updatePano(context, context.camera.camera3D, this);
this.material.updateUniforms(context.camera.camera3D);
updateBackground(this);
}
getNextPano() {
const index = (this.currentPano.index + 1) % this.panos.length;
return this.panos[index];
}
getCurrentPano() {
return this.currentPano;
}
getPreviousPano() {
const index = (this.currentPano.index - 1) % this.panos.length;
return this.panos[index];
}
/**
* Delete background, but doesn't delete OrientedImageLayer.material. For the moment, this material visibility is set to false.
* You need to replace OrientedImageLayer.material applied on each object, if you want to continue displaying them.
* This issue (see #1018 {@link https://github.com/iTowns/itowns/issues/1018}) will be fixed when OrientedImageLayer will be a ColorLayer.
* @param {boolean} [clearCache=false] Whether to clear the layer cache or not
*/
delete(clearCache) {
if (this.background) {
// only delete geometries if it has some
super.delete();
}
if (clearCache) {
this.cache.clear();
}
this.material.visible = false;
console.warn('You need to replace OrientedImageLayer.material applied on each object. This issue will be fixed when OrientedImageLayer will be a ColorLayer. the material visibility is set to false. To follow issue see https://github.com/iTowns/itowns/issues/1018');
}
mostNearPano(position) {
let minDistance = Infinity;
let nearPano;
for (const pano of this.panos) {
const distance = position.distanceTo(pano.position);
if (distance < minDistance) {
minDistance = distance;
nearPano = pano;
}
}
return nearPano;
}
}
export default OrientedImageLayer;