├── .gitignore ├── LICENSE ├── README.md ├── example-iss.html ├── example.html ├── globe.js └── img ├── bump.jpg ├── specular.jpg └── world.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mike van Rossum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Realtime Webgl Globe 2 | 3 | A webgl earth making it easy to add custom shapes at coordinates in realtime. 4 | 5 | ![example gif](https://mikevanrossum.nl/stuff/realtime-webgl-globe/realtime-globe.gif) 6 | 7 | [Demo](https://mikevanrossum.nl/stuff/realtime-webgl-globe/example.html)! 8 | 9 | Or check out [this demo](https://github.com/askmike/realtime-webgl-globe/blob/master/example-iss.html) that shows the position of the International Space Station! 10 | 11 | ## Features 12 | 13 | - Makes it easy to add custom shapes to the globe at lat/long positions. 14 | - Interactive (mousewheel scroll & mouse drags). 15 | - Easy API for adding elements on the globe while it's running. 16 | 17 | ## Basic Usage 18 | 19 | var div = document.getElementById('globe'); 20 | var urls = { 21 | earth: 'img/world.jpg', 22 | bump: 'img/bump.jpg', 23 | specular: 'img/specular.jpg', 24 | } 25 | 26 | // create a globe 27 | var globe = new Globe(div, urls); 28 | 29 | // start it 30 | globe.init(); 31 | 32 | // random data 33 | var data = { 34 | color: '#FF0000', 35 | size: 20, 36 | lat: 52.3747158, // Amsterdam! 37 | lon: 4.8986231, // Amsterdam! 38 | size: 20 39 | }; 40 | 41 | // add a block on Amsterdam 42 | globe.addBlock(data); 43 | 44 | ## API 45 | 46 | * * * 47 | 48 | ## Class: Globe 49 | Realtime Globe is a WebGL based earth globe that 50 | makes it super simple to add shapes in realtime 51 | on specific lat/lon positions on earth. 52 | 53 | ### Globe.init() 54 | 55 | Initializes the globe 56 | 57 | 58 | 59 | ### Globe.zoomRelative(delta) 60 | 61 | Zoom the earth relatively to its current zoom 62 | (passing a positive number will zoom towards 63 | the earth, while a negative number will zoom 64 | away from earth). 65 | 66 | **Parameters** 67 | 68 | - **delta**: `Integer` 69 | 70 | **Returns**: `this` 71 | 72 | ### Globe.zoomTo(altitute) 73 | 74 | Transition the altitute of the camera to a 75 | specific distance from the earth's core. 76 | 77 | **Parameters** 78 | 79 | - **altitute**: `Integer` 80 | 81 | **Returns**: `this` 82 | 83 | ### Globe.zoomImmediatelyTo(altitude) 84 | 85 | Set the altitute of the camera to a specific 86 | distance from the earth's core. 87 | 88 | **Parameters** 89 | 90 | - **altitude**: `Integer` 91 | 92 | **Returns**: `this` 93 | 94 | ### Globe.center(pos) 95 | 96 | Transition the globe from its current position 97 | to the new coordinates. 98 | 99 | **Parameters** 100 | 101 | - **pos**: `Object`, the position 102 | - **pos.lat**: `Float`, latitute position 103 | - **pos.lon**: `Float`, longtitute position 104 | 105 | **Returns**: `this` 106 | 107 | ### Globe.centerImmediate(pos) 108 | 109 | Center the globe on the new coordinates. 110 | 111 | **Parameters** 112 | 113 | - **pos**: `Object`, the position 114 | - **pos.lat**: `Float`, latitute position 115 | - **pos.lon**: `Float`, longtitute position 116 | 117 | **Returns**: `this` 118 | 119 | ### Globe.addLevitatingBlock(data) 120 | 121 | Adds a block to the globe. The globe will spawn 122 | just below the earth's surface and `levitate` 123 | out of the surface until it is fully `out` of the 124 | earth. 125 | 126 | **Parameters** 127 | 128 | - **data**: `Object` 129 | - **data.lat**: `Float`, latitute position 130 | - **data.lon**: `Float`, longtitute position 131 | - **data.size**: `Float`, size of the block 132 | - **data.color**: `String`, color of the block 133 | 134 | **Returns**: `this` 135 | 136 | ### Globe.addBlock(data) 137 | 138 | Adds a block to the globe. 139 | 140 | **Parameters** 141 | 142 | - **data**: `Object` 143 | - **data.lat**: `Float`, latitute position 144 | - **data.lon**: `Float`, longtitute position 145 | - **data.size**: `Float`, size of the block 146 | - **data.color**: `String`, color of the block 147 | 148 | **Returns**: `this` 149 | 150 | ### Globe.removeAllBlocks() 151 | 152 | Remove all blocks from the globe. 153 | 154 | 155 | **Returns**: `this` 156 | 157 | 158 | 159 | * * * 160 | -------------------------------------------------------------------------------- /example-iss.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Realtime WebGL Globe Example 8 | 9 | 18 | 19 | 20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Realtime WebGL Globe Example 8 | 9 | 18 | 19 | 20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | 71 | 72 | -------------------------------------------------------------------------------- /globe.js: -------------------------------------------------------------------------------- 1 | // Realtime WebGL globe 2 | // Copyright (c) 2015 Mike van Rossum 3 | 4 | /** 5 | * Realtime Globe is a WebGL based earth globe that 6 | * makes it super simple to add shapes in realtime 7 | * on specific lat/lon positions on earth. 8 | * 9 | * @class Globe 10 | * @param {HTMLElement} container 11 | * @param {Object} urls - URLs of earth images 12 | * @param {String} urls.earth 13 | * @param {String|undefined} urls.bump (optional) 14 | * @param {String|undefined} urls.specular (optional) 15 | */ 16 | var Globe = function Globe(container, urls) { 17 | var PI = Math.PI; 18 | var PI_HALF = PI / 2; 19 | 20 | // Three.js objects 21 | var camera; 22 | var scene; 23 | var light; 24 | var renderer; 25 | 26 | var earthGeometry; 27 | var earthPosition; 28 | 29 | // camera's distance from center (and thus the globe) 30 | var distanceTarget = 900; 31 | var distance = distanceTarget; 32 | 33 | // camera's position 34 | var rotation = { x: 2, y: 1 }; 35 | var target = { x: 2, y: 1 }; 36 | 37 | // holds currently levitating blocks 38 | var levitatingBlocks = []; 39 | // holds all block references 40 | var blocks = []; 41 | 42 | // What gets exposed by calling: 43 | // 44 | // var globe = [new] Globe(div, urls); 45 | // 46 | // attach public functions to this object 47 | var api = {}; 48 | 49 | /** 50 | * Initializes the globe 51 | * 52 | */ 53 | api.init = function() { 54 | setSize(); 55 | 56 | // Camera 57 | camera = new THREE.PerspectiveCamera(30, w / h, 1, 1000); 58 | camera.position.z = distance; 59 | 60 | // Scene 61 | scene = new THREE.Scene(); 62 | 63 | // Earth geom, used for earth & atmosphere 64 | earthGeometry = new THREE.SphereGeometry(200, 64, 64); 65 | 66 | // Light, reposition close to camera 67 | light = createMesh.directionalLight(); 68 | 69 | // we use this to correctly position camera and blocks 70 | var earth = createMesh.earth(urls); 71 | earthPosition = earth.position; 72 | 73 | // Add meshes to scene 74 | scene.add(earth); 75 | scene.add(createMesh.atmosphere()); 76 | 77 | // Add lights to scene 78 | scene.add(new THREE.AmbientLight(0x656565)); 79 | scene.add(light); 80 | 81 | // Renderer 82 | renderer = new THREE.WebGLRenderer({antialias: true}); 83 | renderer.setSize(w, h); 84 | 85 | // Add scene to DOM 86 | renderer.domElement.style.position = 'absolute'; 87 | container.appendChild(renderer.domElement); 88 | 89 | // DOM event handlers 90 | container.addEventListener('mousedown', handle.drag.start, false); 91 | window.addEventListener('resize', handle.resize, false); 92 | 93 | // Scroll for Chrome 94 | window.addEventListener('mousewheel', handle.scroll, false); 95 | // Scroll for Firefox 96 | window.addEventListener('DOMMouseScroll', handle.scroll, false); 97 | 98 | // Bootstrap render 99 | animate(); 100 | 101 | return this; 102 | } 103 | 104 | var setSize = function() { 105 | w = container.offsetWidth || window.innerWidth; 106 | h = container.offsetHeight || window.innerHeight; 107 | } 108 | 109 | var createMesh = { 110 | 111 | // @param urls Object URLs of images 112 | // 113 | // { 114 | // earth: String URL 115 | // bump: Sting URL [optional] 116 | // specular: String URL [optional] 117 | // } 118 | // 119 | // See 120 | // @link http://learningthreejs.com/blog/2013/09/16/how-to-make-the-earth-in-webgl/ 121 | // @link http://learningthreejs.com/data/2013-09-16-how-to-make-the-earth-in-webgl/demo/index.html 122 | earth: function(urls) { 123 | if(!urls.earth) 124 | throw 'No image URL provided for an earth image'; 125 | 126 | var material = new THREE.MeshPhongMaterial(); 127 | material.map = THREE.ImageUtils.loadTexture(urls.earth); 128 | 129 | if(urls.bump) { 130 | material.bump = THREE.ImageUtils.loadTexture(urls.bump); 131 | material.bumpScale = 0.02; 132 | } 133 | 134 | if(urls.specular) { 135 | material.specularMap = THREE.ImageUtils.loadTexture(urls.specular); 136 | material.specular = new THREE.Color('grey'); 137 | } 138 | 139 | return new THREE.Mesh(earthGeometry, material); 140 | }, 141 | 142 | // See 143 | // @link https://github.com/dataarts/webgl-globe/blob/master/globe/globe.js#L52 144 | // @link http://bkcore.com/blog/3d/webgl-three-js-animated-selective-glow.html 145 | // 146 | // Currently has some issues, especially when zooming out (distance > 900) 147 | atmosphere: function() { 148 | var material = new THREE.ShaderMaterial({ 149 | vertexShader: [ 150 | 'varying vec3 vNormal;', 151 | 'void main() {', 152 | 'vNormal = normalize( normalMatrix * normal );', 153 | 'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', 154 | '}' 155 | ].join('\n'), 156 | fragmentShader: [ 157 | 'varying vec3 vNormal;', 158 | 'void main() {', 159 | 'float intensity = pow( 0.8 - dot( vNormal, vec3( 0, 0, 1.0 ) ), 7.0 );', 160 | 'gl_FragColor = vec4( 0.7, 1.0, 0.7, 1.0 ) * intensity;', 161 | '}' 162 | ].join('\n'), 163 | side: THREE.BackSide, 164 | blending: THREE.AdditiveBlending, 165 | transparent: false 166 | }); 167 | 168 | var mesh = new THREE.Mesh(earthGeometry, material); 169 | mesh.scale.set(1.1, 1.1, 1.1); 170 | return mesh; 171 | }, 172 | 173 | directionalLight: function() { 174 | return new THREE.DirectionalLight(0xcccccc, 0.5); 175 | }, 176 | 177 | 178 | block: function(color) { 179 | return new THREE.Mesh( 180 | new THREE.BoxGeometry(1, 1, 1), 181 | new THREE.MeshLambertMaterial({color: color}) 182 | ); 183 | } 184 | 185 | } 186 | 187 | // Keep track of mouse positions 188 | var mouse = { x: 0, y: 0 }; 189 | var mouseOnDown = { x: 0, y: 0 }; 190 | var targetOnDown = { x: 0, y: 0 }; 191 | 192 | // DOM event handlers 193 | var handle = { 194 | scroll: function(e) { 195 | e.preventDefault(); 196 | 197 | // See 198 | // @link http://www.h3xed.com/programming/javascript-mouse-scroll-wheel-events-in-firefox-and-chrome 199 | if(e.wheelDelta) { 200 | // chrome 201 | var delta = e.wheelDelta * 0.5; 202 | } else { 203 | // firefox 204 | var delta = -e.detail * 15; 205 | } 206 | 207 | api.zoomRelative(delta); 208 | 209 | return false; 210 | }, 211 | 212 | resize: function(e) { 213 | setSize(); 214 | camera.aspect = w / h; 215 | camera.updateProjectionMatrix(); 216 | renderer.setSize(w, h); 217 | }, 218 | 219 | // See 220 | // @link https://github.com/dataarts/webgl-globe/blob/master/globe/globe.js#L273-L334 221 | drag: { 222 | start: function(e) { 223 | e.preventDefault(); 224 | container.addEventListener('mousemove', handle.drag.move, false); 225 | container.addEventListener('mouseup', handle.drag.end, false); 226 | container.addEventListener('mouseout', handle.drag.end, false); 227 | 228 | mouseOnDown.x = -e.clientX; 229 | mouseOnDown.y = e.clientY; 230 | 231 | targetOnDown.x = target.x; 232 | targetOnDown.y = target.y; 233 | 234 | container.style.cursor = 'move'; 235 | }, 236 | move: function(e) { 237 | mouse.x = -e.clientX; 238 | mouse.y = e.clientY; 239 | 240 | var zoomDamp = distance / 1000; 241 | 242 | target.x = targetOnDown.x + (mouse.x - mouseOnDown.x) * 0.005 * zoomDamp; 243 | target.y = targetOnDown.y + (mouse.y - mouseOnDown.y) * 0.005 * zoomDamp; 244 | 245 | target.y = target.y > PI_HALF ? PI_HALF : target.y; 246 | target.y = target.y < - PI_HALF ? - PI_HALF : target.y; 247 | }, 248 | end: function(e) { 249 | container.removeEventListener('mousemove', handle.drag.move, false); 250 | container.removeEventListener('mouseup', handle.drag.end, false); 251 | container.removeEventListener('mouseout', handle.drag.end, false); 252 | container.style.cursor = 'auto'; 253 | } 254 | } 255 | } 256 | 257 | var checkAltituteBoundries = function() { 258 | // max zoom 259 | if(distanceTarget < 300) 260 | distanceTarget = 300; 261 | 262 | // min zoom 263 | else if(distanceTarget > 900) 264 | distanceTarget = 900; 265 | } 266 | 267 | var animate = function() { 268 | requestAnimationFrame(animate); 269 | render(); 270 | } 271 | 272 | var render = function() { 273 | levitateBlocks(); 274 | 275 | // Rotate towards the target 276 | rotation.x += (target.x - rotation.x) * 0.1; 277 | rotation.y += (target.y - rotation.y) * 0.1; 278 | distance += (distanceTarget - distance) * 0.3; 279 | 280 | // determine camera position 281 | set3dPosition(camera, { 282 | x: rotation.x, 283 | y: rotation.y, 284 | altitude: distance 285 | }); 286 | 287 | // Determine light position based 288 | set3dPosition(light, { 289 | x: rotation.x - 150, 290 | y: rotation.y - 150, 291 | altitude: distance 292 | }); 293 | 294 | camera.lookAt(earthPosition); 295 | renderer.render(scene, camera); 296 | } 297 | 298 | // @param Object position (2d lat/lon coordinates) 299 | // @return Object coords (x/y coordinates) 300 | // 301 | // Calculates x, y coordinates based on 302 | // lat/lon coordinates. 303 | var calculate2dPosition = function(coords) { 304 | var phi = (90 + coords.lon) * PI / 180; 305 | var theta = (180 - coords.lat) * PI / 180; 306 | 307 | return { 308 | x: phi, 309 | y: PI - theta 310 | } 311 | } 312 | 313 | // @param Mesh object 314 | // @param Object coords (x/y coordinates in 2d space + altitute) 315 | // 316 | // Calculates 3d position and sets it on mesh 317 | var set3dPosition = function(mesh, coords) { 318 | if(!coords) 319 | coords = mesh.userData; 320 | 321 | var x = coords.x; 322 | var y = coords.y; 323 | var altitude = coords.altitude; 324 | 325 | mesh.position.set( 326 | altitude * Math.sin(x) * Math.cos(y), 327 | altitude * Math.sin(y), 328 | altitude * Math.cos(x) * Math.cos(y) 329 | ); 330 | } 331 | 332 | // Create a block mesh and set its position in 3d 333 | // space just below the earths surface 334 | var createLevitatingBlock = function(properties) { 335 | // create mesh 336 | var block = createMesh.block(properties.color); 337 | 338 | // calculate 2d position 339 | var pos2d = calculate2dPosition(properties); 340 | 341 | block.userData = { 342 | // set 2d position on earth so we can more 343 | // easily recalculate the 3d position 344 | 345 | x: pos2d.x, 346 | y: pos2d.y, 347 | 348 | 349 | altitude: 200 - properties.size / 1.5, 350 | // speed at which block levitates outside 351 | // earth's core 352 | levitation: .1, 353 | 354 | size: properties.size 355 | } 356 | 357 | // calculate 3d position 358 | set3dPosition(block); 359 | 360 | // rotate towards earth 361 | block.lookAt(earthPosition); 362 | 363 | block.scale.z = properties.size; 364 | block.scale.x = properties.size; 365 | block.scale.y = properties.size; 366 | 367 | block.updateMatrix(); 368 | 369 | return block; 370 | } 371 | 372 | // Create a block mesh and set its position in 3d 373 | // space just below the earths surface 374 | var createBlock = function(properties) { 375 | // create mesh 376 | var block = createMesh.block(properties.color); 377 | 378 | // calculate 2d position 379 | var pos2d = calculate2dPosition(properties); 380 | 381 | // add altitute 382 | pos2d.altitude = 200 + properties.size / 2; 383 | 384 | // calculate 3d position 385 | set3dPosition(block, pos2d); 386 | 387 | // rotate towards earth 388 | block.lookAt(earthPosition); 389 | 390 | block.scale.z = properties.size; 391 | block.scale.x = properties.size; 392 | block.scale.y = properties.size; 393 | 394 | block.updateMatrix(); 395 | 396 | return block; 397 | } 398 | 399 | // internal function to levitate all levitating 400 | // blocks each tick. Called on render. 401 | var levitateBlocks = function() { 402 | levitatingBlocks.forEach(function(block, i) { 403 | 404 | var userData = block.userData; 405 | 406 | // if entirely outide of earth, stop levitating 407 | if(userData.altitude > 200 + userData.size / 2) { 408 | levitatingBlocks.splice(i, 1); 409 | return; 410 | } 411 | 412 | userData.altitude += userData.levitation; 413 | set3dPosition(block); 414 | block.updateMatrix(); 415 | }); 416 | } 417 | 418 | // Public functions 419 | 420 | /** 421 | * Zoom the earth relatively to its current zoom 422 | * (passing a positive number will zoom towards 423 | * the earth, while a negative number will zoom 424 | * away from earth). 425 | * 426 | * @param {Integer} delta 427 | * @return {this} 428 | */ 429 | api.zoomRelative = function(delta) { 430 | distanceTarget -= delta; 431 | checkAltituteBoundries(); 432 | 433 | return this; 434 | } 435 | 436 | /** 437 | * Transition the altitute of the camera to a 438 | * specific distance from the earth's core. 439 | * 440 | * @param {Integer} altitute 441 | * @return {this} 442 | */ 443 | api.zoomTo = function(altitute) { 444 | distanceTarget = altitute; 445 | checkAltituteBoundries(); 446 | 447 | return this; 448 | } 449 | 450 | /** 451 | * Set the altitute of the camera to a specific 452 | * distance from the earth's core. 453 | * 454 | * @param {Integer} altitude 455 | * @return {this} 456 | */ 457 | api.zoomImmediatelyTo = function(altitute) { 458 | distanceTarget = distance = altitute; 459 | checkAltituteBoundries(); 460 | 461 | return this; 462 | } 463 | 464 | /** 465 | * Transition the globe from its current position 466 | * to the new coordinates. 467 | * 468 | * @param {Object} pos - the position 469 | * @param {Float} pos.lat - latitute position 470 | * @param {Float} pos.lon - longtitute position 471 | * @return {this} 472 | */ 473 | api.center = function(pos) { 474 | target = calculate2dPosition(pos); 475 | return this; 476 | } 477 | 478 | /** 479 | * Center the globe on the new coordinates. 480 | * 481 | * @param {Object} pos - the position 482 | * @param {Float} pos.lat - latitute position 483 | * @param {Float} pos.lon - longtitute position 484 | * @return {this} 485 | */ 486 | api.centerImmediate = function(pos) { 487 | target = rotation = calculate2dPosition(pos); 488 | return this; 489 | } 490 | 491 | /** 492 | * Adds a block to the globe. The globe will spawn 493 | * just below the earth's surface and `levitate` 494 | * out of the surface until it is fully `out` of the 495 | * earth. 496 | * 497 | * @param {Object} data 498 | * @param {Float} data.lat - latitute position 499 | * @param {Float} data.lon - longtitute position 500 | * @param {Float} data.size - size of the block 501 | * @param {String} data.color - color of the block 502 | * @return {this} 503 | */ 504 | api.addLevitatingBlock = function(data) { 505 | var block = createLevitatingBlock(data); 506 | 507 | scene.add(block); 508 | levitatingBlocks.push(block); 509 | blocks.push(block); 510 | 511 | return this; 512 | } 513 | 514 | /** 515 | * Adds a block to the globe. 516 | * 517 | * @param {Object} data 518 | * @param {Float} data.lat - latitute position 519 | * @param {Float} data.lon - longtitute position 520 | * @param {Float} data.size - size of the block 521 | * @param {String} data.color - color of the block 522 | * @return {this} 523 | */ 524 | api.addBlock = function(data) { 525 | var block = createBlock(data); 526 | 527 | scene.add(block); 528 | blocks.push(block); 529 | 530 | return this; 531 | } 532 | /** 533 | * Remove all blocks from the globe. 534 | * 535 | * @return {this} 536 | */ 537 | api.removeAllBlocks = function() { 538 | blocks.forEach(function(block) { 539 | scene.remove(block); 540 | }); 541 | 542 | blocks = []; 543 | 544 | return this; 545 | } 546 | 547 | 548 | return api; 549 | } -------------------------------------------------------------------------------- /img/bump.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/askmike/realtime-webgl-globe/e215750848b94063ef257121ef5d18432d1249c5/img/bump.jpg -------------------------------------------------------------------------------- /img/specular.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/askmike/realtime-webgl-globe/e215750848b94063ef257121ef5d18432d1249c5/img/specular.jpg -------------------------------------------------------------------------------- /img/world.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/askmike/realtime-webgl-globe/e215750848b94063ef257121ef5d18432d1249c5/img/world.jpg --------------------------------------------------------------------------------