-
Notifications
You must be signed in to change notification settings - Fork 1
Petit tutoriel
Les prérequis minimums pour ce tutoriel sont :
- une certaine familiarité avec du JavaScript "moderne" (ES2015+)
- les outils qui lui sont typiquement liés : Node.js 10.x.x et npm, webpack
- Git
Un point d'entrée possible mais limité peut se trouver ici.
Le but de ce tutoriel, est de voir comment intégrer quelques sources de données, et de manière générale, manipuler itowns. Il ne fait aucunement figure d'autorité. La documentation d'itowns est disponible sur le site du projet.
Le code obtenu en fin de tutoriel est disponible ici.
- Mise en place des outils et de la structure du projet
- Manuellement
-
En utilisant un script[abandonner cette manière de faire ?] - Ajouter itowns et ses dépendances
- Pour débuter...
- Un peu plus avancé
- Miscellaneous
Créer la structure de base du projet, ajouter webpack comme dépendance, et installer le serveur de développement http-server
:
mkdir tuto
cd tuto
npm init
npm install webpack --save-dev
npm install webpack-cli --save-dev
sudo npm install http-server -g
Modifier package.json
pour ajouter des scripts de build dans sa partie "scripts":
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --mode development --progress",
"autobuild": "webpack --mode development --progress --watch"
}
Et webpack.config.js
pour indiquer les sources en entrée et sortie:
const path = require('path');
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, "dist"),
filename: 'bundle.js'
},
};
Ainsi on pourra autobuilder (cad recompiler le bundle à chaque fois qu'on sauvegarde une modification) le projet en se mettant à la racine et en lançant :
~/code/tuto$ npm run autobuild
Et dans un autre terminal, toujours à la base du projet, on lancera le serveur web :
~/code/tuto$ http-server
Dans le navigateur web le projet est alors généralement accessible à l'adresse http://localhost:8080
.
Une autre manière de créer la structure de base du projet est décrite ici. Elle aboutira à un squelette de projet plus conséquent avec un certain nombre de fichiers déjà crées, qu'on modifiera pour suivre ce tutoriel.
Ce tutoriel, au moment de son écriture, a été testé avec la version d'itowns 2.9.0.
imran@imran-RKXXXXXXXXXXXXXXX~/code/js/tuto/master$ npm install itowns
Puis :
npm install [email protected] --save
Outre les habituels fichiers package.json
et webpack.config.js
, caractéristiques d'un développement node/npm/webpack , le projet est structuré de manière classique :
- Un fichier
index.html
à la racine - Un sous-dossier css contenant un fichier
style.css
- Un sous-dossier src pour le code JavaScript
Le fichier html contiendra un élément div
pour la fenêtre de visualisation d'itowns, et une balise <script>
pour le code JavaScript.
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="css/style.css">
<title>Basic tuto</title>
</head>
<body>
<div id="viewerDiv"></div>
<script src="dist/bundle.js"></script>
</body>
</html>
Le fichier style.css
pourra ressembler à ceci :
html {height: 100%}
body { margin: 0; overflow:hidden; height:100%}
#viewerDiv {margin : auto auto; width: 100%; height: 100%; padding: 0;}
Dans le fichier src/main.js
, on va importer itowns comme un module ES6, puis créer le globe.
Un élément <div>
(#viewerDiv dans notre cas) de la page contiendra le viewer, qui sera de type Globe :
il sera crée en utilisant le constructeur itowns.Globeview(viewerDiv, positionInitiale)
qui attend en paramètres le <div>
conteneur, et une position initiale définie via un objet JavaScript :
import * as itowns from 'itowns';
let positionOnGlobe = { longitude: 2.351323, latitude: 48.856712, altitude: 25000000 };
let viewerDiv = document.getElementById('viewerDiv');
let globeView = new itowns.GlobeView(viewerDiv, positionOnGlobe);
En compilant le bundle, et en visionnant le résultat en local, on aura dans notre navigateur un globe, assez vide :
De manière générale, on ajoute un layer à la vue (le globeView
), en utilisant la méthode addLayer
de l'objet GlobeView
, en lui passant un objet JavaScript décrivant ce layer.
On va créer dans src
un sous-répertoire layers
, qui contiendra un tel fichier. Il permettra d'afficher un fond de carte stylisé d'OpenStreetMap. On appellera ce fichier DARK.json
(c'est le même que celui qui est disponible dans les exemples du dépôt itowns ici) :
{
"id": "DARK",
"source": {
"networkOptions": { "crossOrigin" : "anonymous" },
"format": "image/png",
"url": "http://a.basemaps.cartocdn.com/dark_all/${z}/${x}/${y}.png",
"attribution": {
"name":"CARTO",
"url": "https://carto.com/"
},
"tileMatrixSet": "PM"
}
}
Il décrit ici des tuiles images au format "xyz" (voir doc OSM).
On peut importer directement un fichier json dans une variable avec webpack, en utilisant une syntaxe du type
import variable from '/chemin/vers/fichier.json'
/* ou plus simplement, sans l'extension : */
import variable from '/chemin/vers/fichier'
On crée alors un Layer à partir de ce fichier json, en en faisant d'abord une source.
Notre fichier src/main.js
ressemble alors à :
import * as itowns from 'itowns';
import darkJson from './layers/dark'
let positionOnGlobe = { longitude: 2.351323, latitude: 48.856712, altitude: 25000000 };
let viewerDiv = document.getElementById('viewerDiv');
let globeView = new itowns.GlobeView(viewerDiv, positionOnGlobe);
let darkSource = new itowns.TMSSource(darkJson.source);
let darkLayer = new itowns.ColorLayer('DARK', {
source: darkSource,
});
globeView.addLayer(darkLayer);
Et on a en "recompilant" :
On va maintenant ajouter des bâtiments de la BDTopo, toujours en utilisant la méthode addLayer
, mais en utilisant maintenant une source de type WFS.
Pour se faire, toujours dans src/layers/
on va créer un fichier bati.js
, dans lequel on va décrire la couche WFS (de manière similaire à ce qu'on peut voir dans les exemples d'itowns, comme ici) :
import * as itowns from 'itowns';
import * as THREE from 'three';
const wfsSource = new itowns.WFSSource({
url: 'http://wxs.ign.fr/3ht7xcw6f7nciopo16etuqp2/geoportail/wfs?',
version: '2.0.0',
typeName: 'BDTOPO_BDD_WLD_WGS84G:bati_remarquable,BDTOPO_BDD_WLD_WGS84G:bati_indifferencie,BDTOPO_BDD_WLD_WGS84G:bati_industriel',
projection: 'EPSG:4326',
zoom: { min: 15, max: 15 },
format: 'application/json',
});
let batiLayer = new itowns.GeometryLayer('Buildings', new THREE.Group(), {
update: itowns.FeatureProcessing.update,
convert: itowns.Feature2Mesh.convert({
extrude: (p) => p.hauteur,
color: () => new THREE.Color(0xcab0ff),
}),
overrideAltitudeInToZero: true,
source: wfsSource
});
export default batiLayer;
Les attributs 'update' et 'convert' font référence à des fonctions de callbacks qui seront utilisées pour le rendu et la mise à jour de l'affichage des features récupérées du flux.
Seule 'update' est obligatoire, et on lui passera généralement une fonction pré existante, itowns.FeatureProcessing.update
.
Pour l'attribut 'convert', on pourra lui passer la fonction itowns.Feature2Mesh.convert
avec un certain nombre d'options, pour l'extrusion, la couleur et l'altitude (Ces options sont elles-mêmes des fonctions, pouvant par exemple utiliser une propriété de la feature, comme ici pour la fonction d'extrusion, qui cherche une propriété 'hauteur').
Dans notre main.js
, on importe l'objet bati
et on l'ajoute comme précédemment :
import * as itowns from 'itowns';
import darkJson from './layers/dark'
import batiLayer from './layers/bati'
let positionOnGlobe = { longitude: 2.351323, latitude: 48.856712, altitude: 25000000 };
let viewerDiv = document.getElementById('viewerDiv');
let globeView = new itowns.GlobeView(viewerDiv, positionOnGlobe);
let darkSource = new itowns.TMSSource(darkJson.source);
let darkLayer = new itowns.ColorLayer('DARK', {
source: darkSource,
});
globeView.addLayer(darkLayer);
globeView.addLayer(batiLayer)
En "recompilant", puis en zoomant suffisamment, on a :
Ajouter un layer se fait de manière asynchrone : globeView.addLayer
renvoie en fait une Promise
, et il n'y a pas de garantie sur l'ordre d'exécution des instructions.
On va organiser le code pour indiquer le fait que globe soit initialisé et les données chargées.
Pour se faire, on attache un listener sur l'événement GLOBE_VIEW_EVENTS.GLOBE_INITIALIZED
, et on gère les promesses avec des async/await (ES2017, supportés par les navigateurs récents).
On peut aussi utiliser l'objet Promise
, comme dans les exemples d'itowns, voir ici ou ailleurs...).
Notre main.js
devient alors :
import * as itowns from 'itowns';
import darkJson from './layers/dark'
import batiLayer from './layers/bati'
let positionOnGlobe = { longitude: 2.351323, latitude: 48.856712, altitude: 25000000 };
let viewerDiv = document.getElementById('viewerDiv');
let globeView = new itowns.GlobeView(viewerDiv, positionOnGlobe);
let darkSource = new itowns.TMSSource(darkJson.source);
let darkLayer = new itowns.ColorLayer('DARK', {
source: darkSource,
});
let layersLoaded = async function loadLayers() {
console.log('loading dark');
const dark = await globeView.addLayer(darkLayer).then(() => true).catch((r) => { console.error(r); return false });
console.log('--dark done');
console.log('loading bati');
const batitopo = await globeView.addLayer(batiLayer).then(() => true).catch((r) => { console.error(r); return false });
console.log('--bati done');
return dark && batitopo;
}();
globeView.addEventListener(itowns.GLOBE_VIEW_EVENTS.GLOBE_INITIALIZED, async () => {
console.log('globe initialized');
if (await layersLoaded) {
console.log('layers loaded!');
} else {
console.error('something bad happened during layers loading..');
}
});
et on doit voir dans la console du navigateur quelque chose comme :
main.js:17 loading dark
main.js:19 --dark done
main.js:20 loading bati
main.js:22 --bati done
main.js:27 globe initialized
main.js:29 layers loaded!
Créer un répertoire data
à la base du projet, et y copier ce fichier json.
C'est un ensemble de multipolygones, représentant quelques parcelles.
On va ajouter dans src/layers
, un fichier parcelles_raster.js
qui va créer un Layer rasterisé à partir du GeoJson de parcelles :
import * as itowns from 'itowns';
import parcellesJson from '../../data/parcelles_sample'
const getJsonSource = async function () {
let geojson = await itowns.GeoJsonParser.parse(parcellesJson, {
buildExtent: true,
crsIn: 'EPSG:4326',
crsOut: 'EPSG:4326',
mergeFeatures: true,
withNormal: false,
withAltitude: false,
});
const source = new itowns.FileSource({ parsedData: geojson, projection: 'EPSG:4326' });
return source;
}
async function buildParcellesLayer () {
const parcellesLayer = new itowns.ColorLayer('parcelles', {
name: 'parcelles',
transparent: true,
zoom: {
min: 13,
max: 14
},
style: {
fill: "red",
fillOpacity: 0.8,
stroke: "cyan",
},
source: await getJsonSource(),
});
return parcellesLayer;
}
export default buildParcellesLayer;
c'est assez similaire à ce qu'on a fait précédemment, avec un layer de type ColorLayer
dont la source est de type FileSource
.
On peut styliser à minima avec un attribut "style" correspondant à un sous-ensemble d'attributs de ce que l'on peut faire avec <canvas>
.
On importe la fonction pour créer le Layer dans le main.js
, et on change les coordonnées de départ pour centrer la vue sur les parcelles (du côté de Joinville-le-Pont) :
/* ... */
import buildParcellesLayer from './layers/parcelles_raster';
/* ... */
let positionOnGlobe = { longitude: 2.46315, latitude: 48.819609, altitude: 5500 };
/*...... */
let layersLoaded = async function loadLayers() {
console.log('loading dark');
const dark = await globeView.addLayer(DARK).then(() => true).catch((r) => { console.error(r); return false });
console.log('--dark done');
console.log('loading bati');
const batitopo = await globeView.addLayer(bati).then(() => true).catch((r) => { console.error(r); return false });
console.log('--bati done');
console.log('loading parcelles');
let parcelles_raster = await buildParcellesLayer();
const parcelles = await globeView.addLayer(parcelles_raster).then(() => true).catch((r) => { console.error(r); return false });
console.log('--parcelles done');
return dark && batitopo && parcelles;
}();
/* ... */
On se basera sur le code disponible dans les exemples de itowns, en le modifiant, légèrement.
Créer un sous-répertoire GUI
dans src
, puis y copier le fichier GuiTools.js
disponible ici.
Le code repose sur l'utilisation de la librairie dat.gui
, et présume du fait que l'objet dat.gui
soit disponible globalement (ainsi que itowns d'ailleurs).
On va fonctionner différemment des exemples du dépôt GitHub de itowns qui mettent dans le fichier html une référence vers la librairie : on va plutôt ajouter la dépendance avec npm et on l'importera comme un module.
imran@imran-RKXXXXXXXXXXXXXXX~/code/js/tuto$ npm install --save dat.gui
Puis, on l'importe explicitement au début du fichier GuiTools.js
. On aura également besoin d'importer itowns.
On exportera GuiTools
à la fin pour pouvoir l'utiliser dans notre code principal :
/* GuiTools.js */
/* ...*/
import dat from 'dat.gui';
import * as itowns from 'itowns';
/* ...*/
export default GuiTools;
Ce constructeur qu'on vient d'exporter attend au minimum l'identifiant de l'élément qui va contenir l'interface, et présume que la vue (le globeView ici) est représentée par une variable globale viewerDiv
. On laissera les choses en l'état ici...
On va juste compléter notre fichier css/style.css
afin de faire apparaître l'élément de menu, qu'on identifiera par menuDiv
:
#menuDiv {
position: absolute;
top: 0;
margin-left: 0;
}
@media (max-width: 600px) {
#menuDiv {
display: none;
}
}
Enfin, dans le main.js
, on va importer GuiTools
, l'utiliser pour créer un élément de menu appelé menuDiv
, et ajouter des entrées correspondant aux layers ajoutés, une fois ces derniers chargés, avec la méthode addLayersGUI
:
import * as itowns from 'itowns';
import darkJson from './layers/dark'
import batiLayer from './layers/bati'
import buildParcellesLayer from './layers/parcelles_raster';
import GuiTools from './GUI/GuiTools'
let positionOnGlobe = { longitude: 2.46315, latitude: 48.819609, altitude: 5500 };
let viewerDiv = document.getElementById('viewerDiv');
let globeView = new itowns.GlobeView(viewerDiv, positionOnGlobe);
const menuGlobe = new GuiTools('menuDiv', globeView);
let darkSource = new itowns.TMSSource(darkJson.source);
let darkLayer = new itowns.ColorLayer('DARK', {
source: darkSource,
});
let loadLayers = async function() {
console.log('loading dark');
const dark = await globeView.addLayer(darkLayer).then(() => true).catch((r) => { console.error(r); return false });
console.log('--dark done');
console.log('loading bati');
const batitopo = await globeView.addLayer(batiLayer).then(() => true).catch((r) => { console.error(r); return false });
console.log('--bati done');
console.log('loading parcelles');
let parcelles_raster = await buildParcellesLayer();
const parcelles = await globeView.addLayer(parcelles_raster).then(() => true).catch((r) => { console.error(r); return false });
console.log('--parcelles done');
return dark && batitopo && parcelles;
}();
globeView.addEventListener(itowns.GLOBE_VIEW_EVENTS.GLOBE_INITIALIZED, async () => {
console.log('globe initialized');
if (await layersLoaded) {
console.log('layers loaded!');
menuGlobe.addLayersGUI();
} else {
console.error('something bad happened during layers loading..');
}
});
Nous allons modifier cette méthode addLayersGUI
afin qu'elle prenne en compte les couches vectorielles, en nous inspirant du code existant. On crée sur le modèle des méthodes existantes, addGeometryLayerGUI
et addGeometryLayersGUI
:
/* ./GUI/GuiTools.js */
/* ... */
GuiTools.prototype.addLayersGUI = function fnAddLayersGUI() {
function filterColor(l) { return l.isColorLayer; }
function filterElevation(l) { return l.isElevationLayer; }
/* ajout */
const filterGeometry = l => l.isGeometryLayer && l.id !== 'globe' && l.id !== 'atmosphere';
/******/
this.addImageryLayersGUI(this.view.getLayers(filterColor));
this.addElevationLayersGUI(this.view.getLayers(filterElevation));
/* ajout */
this.addGeometryLayersGUI(this.view.getLayers(filterGeometry));
/******/
// eslint-disable-next-line no-console
console.info('menu initialized');
};
GuiTools.prototype.addGeometryLayerGUI = function addGeometryLayerGUI(layer) {
if (this.geometryGui.hasFolder(layer.id)) { return; }
const folder = this.geometryGui.addFolder(layer.id);
folder.add(layer, 'visible').name('Visible').onChange(() => this.view.notifyChange(layer));
folder.add(layer, 'opacity', 0, 1).name('Opacity').onChange(() => this.view.notifyChange(layer));
folder.add(layer, 'wireframe').name('Wireframe').onChange(() => this.view.notifyChange(layer));
};
GuiTools.prototype.addGeometryLayersGUI = function addGeometryLayersGUI(layers) {
var i;
for (i = 0; i < layers.length; i++) {
this.addGeometryLayerGUI(layers[i]);
}
};
/* ... */
En n'oubliant pas de compléter le constructeur avec un élément geometryGui :
function GuiTools(domId, view, w) {
var width = w || 245;
this.gui = new dat.GUI({ autoPlace: false, width: width });
this.gui.domElement.id = domId;
viewerDiv.appendChild(this.gui.domElement);
this.colorGui = this.gui.addFolder('Color Layers');
/******** ajout ici ********/
this.geometryGui = this.gui.addFolder('Geometry Layers');
/**************************/
this.elevationGui = this.gui.addFolder('Elevation Layers');
if (view) {
this.view = view;
view.addEventListener('layers-order-changed', (function refreshColorGui() {
var i;
var colorLayers = view.getLayers(function filter(l) { return l.type === 'color'; });
for (i = 0; i < colorLayers.length; i++) {
this.removeLayersGUI(colorLayers[i].id);
}
this.addImageryLayersGUI(colorLayers);
}).bind(this));
}
}
Ce qui nous permet d'avoir un petit menu permettant d'afficher/cacher, changer l'opacité de nos couches :
On commencera par ajouter ce fichier geojson au répertoire data
, il contient des multipolygones de cuboïdes représentant des bâtiments simulés.
Dans src/layers
on va créer un fichier 'json_geom.js
import * as itowns from 'itowns';
import * as THREE from 'three';
import simulsJson from '../../data/simuls_sample.json'
const simulSource = new itowns.FileSource({
fetchedData: simulsJson,
crsOut: 'EPSG:4326', //view.tileLayer.extent.crs,
projection: 'EPSG:4326',
format: 'application/json',
zoom: { min: 17, max: 17 },
});
const simulLayer = new itowns.GeometryLayer('simuls', new THREE.Group(), {
update: itowns.FeatureProcessing.update,
convert: itowns.Feature2Mesh.convert({
//altitude: () => 1,
extrude: (p) => { console.log("pppp", p); return 30},
color: () => new THREE.Color(0xffb00b),
batchId: (p, fId) => { console.log("fffId", fId) ; fId }
}),
//overrideAltitudeInToZero: true,
mergeFeatures: false,
source: simulSource
});
export {simulLayer}
Puis dans le main.js
, après avoir importé notre couche
import { simulLayer } from './layers/json_geom'
On complète notre fonction loadLayers
:
let loadLayers = async function() {
console.log('loading dark');
const dark = await globeView.addLayer(darkLayer).then(() => true).catch((r) => { console.error(r); return false });
console.log('--dark done');
console.log('loading bati');
const batitopo = await globeView.addLayer(batiLayer).then(() => true).catch((r) => { console.error(r); return false });
console.log('--bati done');
console.log('loading parcelles');
let parcelles_raster = await buildParcellesLayer();
const parcelles = await globeView.addLayer(parcelles_raster).then(() => true).catch((r) => { console.error(r); return false });
console.log('--parcelles done');
console.log('loading bati simulé');
let simuls = await globeView.addLayer(simulLayer).then(() => true).catch((r) => { console.error(r); return false });
console.log('--bati simulé done');
return dark && batitopo && parcelles && simuls;
}();
On obtient :
Nous allons maintenant voir comment récupérer les informations des features qu'on a chargées, à la fois pour la couche rasterisée et la couche vectorielle.
Pour se faire, on affichera les informations récupérées comme des éléments d'une liste non ordonnée, identifiée par #info
à l'intérieur d'une div #batchInformationDiv
. On va les ajouter dans la balise body de index.html
:
<div id="batchInformationDiv">
<p>
<b>Infos</b>
</p>
<ul id="info">
</ul>
</div>
qu'on pourra styliser en complétant css/style.css
par exemple avec :
#batchInformationDiv {
position: absolute;
z-index: 0;
top: 0;
right: 0;
color: white;
font: 11px 'Lucida Grande',sans-serif;
line-height: normal;
text-shadow: 0 -1px 0 #111;
padding: 0 1rem;
background: #1a1a1a;
border: 1px solid #2c2c2c;
opacity: 0.8;
}
#batchInformationDiv > p {
margin: 0.5rem 0;
}
#batchInformationDiv > ul {
padding: 0 1rem;
}
On va écrire une fonction pickingRaster
qu'on ajoutera sur l'évènement 'mousemove', afin de récupérer l'objet intersectant les coordonnées de la souris sur la couche qui nous intéresse.
window.addEventListener('mousemove', pickingRaster, false);
On utilisera une méthode de GlobeControls
pour déterminer les coordonnées géographiques de la souris (globeView.controls.pickGeoPosition
), et une fonction utilitaire disponible dans itowns pour récupérer les features intersectant la couche rasterisée (itowns.FeaturesUtils.filterFeaturesUnderCoordinate
).
function pickingRaster(event) {
let layer = globeView.getLayers(l => l.name == 'parcelles')[0];
if (layer.visible == false)
return;
let geocoord = globeView.controls.pickGeoPosition(globeView.eventToViewCoords(event));
if (geocoord === undefined)
return;
let result = itowns.FeaturesUtils.filterFeaturesUnderCoordinate(geocoord, layer.source.parsedData, 5);
htmlInfo.innerHTML = 'Parcelle';
htmlInfo.innerHTML += '<hr>';
if (result[0] !== undefined) {
const props = result[0].geometry.properties;
for (const k in props) {
if (k === 'bbox' || k === 'geometry_name')
continue;
htmlInfo.innerHTML += '<li>' + k + ': ' + props[k] + '</li>';
}
}
}
htmlInfo
étant la liste dans laquelle nous allons écrire les propriétés des features récupérées.
let htmlInfo = document.getElementById('info');
Pour une couche géométrique, une fonction de picking est assez similaire mais respose sur la méthode globeView.pickObjectsAt
permettant de récupérer les objets intersectés en passant directement l'événement de la souris et l'id de la couche.
function pickingGeomLayer(event) {
const layer_is_visible = globeView.getLayers(l => l.id === 'simuls')[0].visible;
if (!layer_is_visible)
return;
let results = globeView.pickObjectsAt(event, 5, 'simuls');
if (results.length < 1)
return;
const id = results[0].object.geometry.attributes.batchId.array[results[0].face.a]
const props = results[0].object.feature.geometry[id].properties;
htmlInfo.innerHTML = 'Batiment';
htmlInfo.innerHTML += '<hr>';
for (const k in props) {
htmlInfo.innerHTML += '<li><b>' + k + ':</b> [' + props[k] + ']</li>';
}
};
Cependant, on doit récupérer un attribut identifiant précisément la géométrie "picked" dans le tableau des features intersectées, et pour se faire, on doit ajouter cet identifiant manuellement quand on crée la couche. Par exemple, si on l'appelle batchId
:
const GeomLayer = new itowns.GeometryLayer('geomLayer', new THREE.Group(), {
update: itowns.FeatureProcessing.update,
convert: itowns.Feature2Mesh.convert({
altitude: () => 0,
extrude: () => 10,
color: () => new THREE.Color(0xb00bff),
batchId: (p, featureId) => featureId
}),
overrideAltitudeInToZero: true,
source: someSource
});
On ajoute comme précédemment cette fonction comme listener sur l'événement 'mousemove' et main.js
au final ressemble à :
import * as itowns from 'itowns';
import darkJson from './layers/dark'
import batiLayer from './layers/bati'
import buildParcellesLayer from './layers/parcelles_raster';
import GuiTools from './GUI/GuiTools'
let positionOnGlobe = { longitude: 2.46315, latitude: 48.819609, altitude: 5500 };
let viewerDiv = document.getElementById('viewerDiv');
let globeView = new itowns.GlobeView(viewerDiv, positionOnGlobe);
const menuGlobe = new GuiTools('menuDiv', globeView);
const htmlInfo = document.getElementById('info');
let darkSource = new itowns.TMSSource(darkJson.source);
let darkLayer = new itowns.ColorLayer('DARK', {
source: darkSource,
});
let loadLayers = async function() {
console.log('loading dark');
const dark = await globeView.addLayer(darkLayer).then(() => true).catch((r) => { console.error(r); return false });
console.log('--dark done');
console.log('loading bati');
const batitopo = await globeView.addLayer(batiLayer).then(() => true).catch((r) => { console.error(r); return false });
console.log('--bati done');
console.log('loading parcelles');
let parcelles_raster = await buildParcellesLayer();
const parcelles = await globeView.addLayer(parcelles_raster).then(() => true).catch((r) => { console.error(r); return false });
console.log('--parcelles done');
return dark && batitopo && parcelles;
}();
globeView.addEventListener(itowns.GLOBE_VIEW_EVENTS.GLOBE_INITIALIZED, async () => {
console.log('globe initialized');
if (await loadLayers) {
console.log('layers loaded!');
menuGlobe.addLayersGUI();
window.addEventListener('mousemove', pickingRaster, false);
} else {
console.error('something bad happened during layers loading..');
}
});
function pickingRaster(event) {
let layer = globeView.getLayers(l => l.name == 'parcelles')[0];
if (layer.visible == false)
return;
let geocoord = globeView.controls.pickGeoPosition(globeView.eventToViewCoords(event));
if (geocoord === undefined)
return;
let result = itowns.FeaturesUtils.filterFeaturesUnderCoordinate(geocoord, layer.source.parsedData, 5);
htmlInfo.innerHTML = 'Parcelle';
htmlInfo.innerHTML += '<hr>';
if (result[0] !== undefined) {
const props = result[0].geometry.properties;
for (const k in props) {
if (k === 'bbox' || k === 'geometry_name')
continue;
htmlInfo.innerHTML += '<li>' + k + ': ' + props[k] + '</li>';
}
}
}
Ce qui permet de récupérer les informations de la couche raster..
... et de la couche géométrique ([not yet updated])
Afin de voir comment intégrer un shader, nous allons ajouter un objet 3d à la scène, et "l'habiller" avec un shader.
On commence par ajouter dans src/utils
un fichier src/utils/CreateMesh.js
, dans lequel nous allons créer une fonction addMeshToScene
qui ajoutera un mesh directement à la scene du GlobeView :
function addMeshToScene(globeView) {
let position = new THREE.Vector3(4203699.112252481, 180828.76359087773, 4777410.692429126);
let mesh = generateMesh(position);
globeView.scene.add(mesh);
globeView.myObj = mesh;
globeView.notifyChange(true);
}
Cette fonction délègue la création du mesh à une sous-fonction createMesh
qui pour l'instant va créer un objet cylindrique simple, avec un material
basique :
function generateMesh(position) {
let geom = new THREE.CylinderBufferGeometry(50, 20, 3, 8);
let risimat = new THREE.MeshBasicMaterial({ color: 0xff2142 });
let mesh = new THREE.Mesh(geom, risimat);
mesh.position.copy(position);
mesh.lookAt(new THREE.Vector3(0, 0, 0));
mesh.rotateX(Math.PI / 2);
// // update coordinate of the mesh
mesh.updateMatrixWorld();
return mesh;
}
On exporte addMeshToScene
et on l'importe dans le main.js
, puis on l'appelle après le chargement des couches, ce qui donne :
import * as itowns from 'itowns';
/** .. **/
import { addMeshToScene } from './utils/CreateMesh'
/*
...
...
...
*/
globeView.addEventListener(itowns.GLOBE_VIEW_EVENTS.GLOBE_INITIALIZED, async () => {
console.log('globe initialized');
if (await layersLoaded) {
console.log('layers loaded!');
menuGlobe.addLayersGUI();
addMeshToScene(globeView);
window.addEventListener('mousemove', pickingRaster, false);
} else {
console.error('something bad happened during layers loading..');
}
});
Ce qui donne quelque chose comme :
Puis, nous allons créer une fonction createMaterial(vShader, fShader)
qui va prendre en entrée un vertex shader ainsi qu'un fragment shader, et qui renverra un THREE.ShaderMaterial
qu'on utilisera comme material de notre mesh. Pour que l'exemple reste simple, on va fixer les variables de types uniforms
que l'on passe aux shaders :
function createMaterial(vShader, fShader) {
let uniforms = {
time: {type: 'f', value: 0.2},
resolution: {type: "v2", value: new THREE.Vector2()},
};
uniforms.resolution.value.x = window.innerWidth;
uniforms.resolution.value.y = window.innerHeight;
let meshMaterial = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vShader,
fragmentShader: fShader,
transparent: true,
opacity: 0.7,
side: THREE.DoubleSide
});
return meshMaterial;
}
Dans le code des shaders, on aura besoin d'utiliser un logarithmic depth buffer, ce qui se fait en ajoutant des #include
dans le corps du code des shaders, comme ci-après.
On va passer les shaders sous forme de chaînes de caractères. Un vertex shader très simple, qui ne faite que renvoyer la position :
const vertexShader = `
#include <logdepthbuf_pars_vertex>
uniform float time;
varying vec4 modelpos;
void main(){
modelpos = vec4(position, 1.0);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
#include <logdepthbuf_vertex>
}
`;
et un fragment shader (pris sur internet...) :
const fragmentShader = `
#include <logdepthbuf_pars_fragment>
uniform float time;
uniform vec2 resolution;
varying vec4 modelpos;
#define PI 3.14159
#define TWO_PI (PI*2.0)
#define N 68.5
void main(){
#include <logdepthbuf_fragment>
vec2 center = (modelpos.xy);
center.x=-10.12*sin(time);
center.y=-10.12*cos(time);
vec2 v = (modelpos.xy) / min(resolution.y,resolution.x) * 45.0;
v.x=v.x-10.0;
v.y=v.y-200.0;
float col = 0.0;
for(float i = 0.0; i < N; i++){
float a = i * (TWO_PI/N) * 61.95;
col += cos(TWO_PI*(v.y * cos(a) + v.x * sin(a) + sin(time*0.004)*100.0 ));
}
col /= 5.0;
gl_FragColor = vec4(col*1.0, -col*1.0,-col*4.0, 1.0);
}
`;
On change le material du mesh dans generateMesh
par celui qui utilise les shaders :
let shadMat = createMaterial(vertexShader, fragmentShader);
function generateMesh(position) {
let geom = new THREE.CylinderBufferGeometry(50, 20, 3, 8);
// let risimat = new THREE.MeshBasicMaterial({ color: 0xff2142 });
let mesh = new THREE.Mesh(geom, shadMat);
mesh.position.copy(position);
mesh.lookAt(new THREE.Vector3(0, 0, 0));
mesh.rotateX(Math.PI / 2);
// // update coordinate of the mesh
mesh.updateMatrixWorld();
return mesh;
}
Ce qui donne :
On pourra animer la "texture" en modifiant la valeur de la variable time passée au shader (on joue aussi sur la rotation du mesh dans cet exemple..) en créant dans le main.js
une fonction qui va utiliser la méthode requestAnimationFrame.
Cette méthode permettant d'appeler la fonction qu'on lui a passée en paramètres à chaque rafraîchissement d'écran :
function animate(){
let cyl = globeView.myObj;
cyl.rotation.z = Math.sin(time) * 2.0;
cyl.material.uniforms.time.value = time;
cyl.updateMatrixWorld();
time += 0.01;
if (time > 2*Math.PI){
time = 0.1;//-Math.PI/2;
}
globeView.notifyChange(true);
requestAnimationFrame(animate);
};
On appelle ensuite cette fonction dans le corps de notre programme, une fois les couches et le mesh chargés :
let time = 0;
globeView.addEventListener(itowns.GLOBE_VIEW_EVENTS.GLOBE_INITIALIZED, async () => {
console.log('globe initialized');
if (await layersLoaded) {
console.log('layers loaded!');
menuGlobe.addLayersGUI();
addMeshToScene(globeView);
window.addEventListener('mousemove', pickingRaster, false);
animate();
} else {
console.error('something bad happened during layers loading..');
}
});
That's all folks !
C'est le même principe que pour un GeoJSON, on crée une source pour fetcher le fichier Shape, et on crée une couche ColorLayer à partir de cette source.
Par exemple, on pourra récupérer les fichiers d'un shapefile disponible ici (couche de batiments près de la Roche sur Yon) et les mettre dans le répertoire data
, puis dans src/layers
on créera le fichier shape_bati.js
:
import * as itowns from 'itowns';
const getShapeSource = async function(){
const res = await itowns.Fetcher.multiple('data/test/batis_from_the_deep', {
arrayBuffer: ['shp', 'dbf', 'shx'],
text: ['prj'],
});
const feature = await itowns.ShapefileParser.parse(res, { buildExtent: true, crsOut: 'EPSG:4326' });
const source = new itowns.FileSource({ parsedData: feature, zoom: { min: 14, max: 21 }, projection: 'EPSG:4326' });
return source;
}
async function buildShapeRasterLayer() {
const shapeSource = await getShapeSource();
const shapeLayer = new itowns.ColorLayer('deepLearnedbatis', { source: shapeSource, style: {fill : 'green', stroke: 'yellow'} } );
return shapeLayer;
}
export { buildShapeRasterLayer }
Dans le main.js
on importera la méthode buildShapeRasterLayer
pour construire la couche et l'ajouter au GlobeView, comme précédemment :
/* ... */
import {buildShapeRasterLayer} from './layers/shape_batis'
/* ... */
/* ... */
let loadLayers = async function() {
console.log('loading dark');
const dark = await globeView.addLayer(darkLayer).then(() => true).catch((r) => { console.error(r); return false });
console.log('--dark done');
console.log('loading bati');
const batitopo = await globeView.addLayer(batiLayer).then(() => true).catch((r) => { console.error(r); return false });
console.log('--bati done');
console.log('loading parcelles');
let parcelles_raster = await buildParcellesLayer();
const parcelles = await globeView.addLayer(parcelles_raster).then(() => true).catch((r) => { console.error(r); return false });
console.log('--parcelles done');
console.log('loading bati simulé');
let simuls = await globeView.addLayer(simulLayer).then(() => true).catch((r) => { console.error(r); return false });
console.log('--bati simulé done');
console.log('loading shapes for rasterization')
let batisFromShp = await buildShapeRasterLayer();
let shapes = await globeView.addLayer(batisFromShp).then(() => true).catch((r) => { console.error(r); return false });
//const shapes = true
console.log('--shapefile done');
return dark && batitopo && parcelles && simuls && shapes;
}();