PageSourceSearch

https://survey.com/wp-content/themes/hello-theme-child/map-assets/miniature.earth.js?ver=1790284331

js survey.com collected 2026-09-24 22:28:59 UTC 122,324 bytes, 5,396 lines download raw bytes

1function Earth( element, options ) {
2
3	if ( typeof element == "string" ) {
4		element = document.getElementById( element );
5	}
6	
7	if ( ! element ) {
8		return false;
9	}
10	
11	
12	// check webgl support
13	
14	if ( ! Earth.isSupported( options.legacySupportIE11 ) ) {		
15		element.classList.add( "earth-show-fallback" );
16		return false;
17	}
18	
19	
20	// remove fallback
21	
22	var fallback = element.querySelector( ".earth-fallback" );
23	if ( fallback ) fallback.style.display = "none";
24	
25	
26	// add required css styles
27	
28	if ( ! Earth.cssAdded && Earth.css ) {
29		Earth.addCss();		
30		Earth.cssAdded = true;
31	}
32	
33	
34	// add default meshes
35	
36	if ( ! Earth.meshesAdded && Earth.markerObj ) {
37		Earth.addMesh( Earth.markerObj );
38		Earth.meshesAdded = true;		
39	}
40	
41	
42	// add container css class if not present
43	
44	element.classList.add( "earth-container" );
45
46	
47	// reference to EarthInstance
48	element.earth = this;
49	
50	// reference to dom element
51	this.element = element;
52	
53	
54	// default options
55	
56	var defaults = {
57		
58		isEarth : true,
59		quality: this.getQuality(),
60
61		location: { lat: 0, lng: 0 },
62		
63		mapLandColor : '#F4F4F4',
64		mapSeaColor : '#0099FF',
65		mapBorderColor : '',
66		mapBorderWidth : 0.3,
67		mapStyles : '',
68		
69		mapSvg : '',
70		mapImage : '',
71		
72		draggable : true,
73		grabCursor : true,
74		dragMomentum : true,
75		dragDamping : 0.7,
76		dragPolarLimit : 0.3,
77		
78		polarLimit : 0.3,
79		
80		autoRotate : false,
81		autoRotateSpeed : 1,
82		autoRotateSpeedUp : 0,
83		autoRotateDelay : 1000,
84		autoRotateStart : 1000,
85		autoRotateEasing : 'in-quad',
86		
87		zoom: 1,
88		zoomable : false,
89		zoomMin: 0.5,
90		zoomMax: 1.25,
91		zoomSpeed : 1,
92		
93		light: 'simple', // none, simple, sun
94		lightAmbience: ( options.light == 'none' ) ? 1 : 0.5,
95		lightIntensity: 0.5,
96		lightColor: '#FFFFFF',
97		lightGroundColor: '#999999',
98		sunLocation : { lat: 0, lng: 0 },
99		sunDirection : false,
100		shadows: ( options.light == 'sun' ),
101		
102		shininess: 0.1,
103		transparent : false,
104		innerOpacity : 1,
105		innerColor : '#FFFFFF',
106		
107		paused : false,
108		showHotspots : false,
109		mapHitTest : false,
110
111	};
112	
113	this.options = Object.assign(defaults, options);
114	
115	this.animations = [];
116	this.overlays = [];
117	this.occludables = [];
118	
119	this.ready = false;
120	this.deltaTime = 0;
121	
122	this.goAnimation = null;
123	this.zoomAnimation = null;
124	
125	this.dragging = false;
126	
127	this.autoRotating = false;
128	this.autoRotateTime = 0;
129
130	this.mouseOver = false;
131	this.mouseOverEarth = false;
132	this.mouseOverObject = null;	
133
134	this.docMousePosition = false;
135	this.lastDocMousePosition = false;
136	this.mousePosition = false;
137	this.lastMousePosition = false;
138	this.lastMouseTime = 0;
139	this.mouseVelocity = new THREE.Vector2();
140	
141	this.momentum = new THREE.Vector2();
142	
143	this.elementSize = new THREE.Vector2();
144	this.elementCenter = new THREE.Vector2();
145	this.bounds = {
146		left: 0,
147		top: 0,
148		bottom: 0,
149		right: 0,
150		width: 1,
151		height: 1
152	};
153	this.radius = 0;
154
155	this.init();
156	
157	return this;
158	
159}
160
161
162
163
164Earth.ClassicEventDispatcher = function() {};
165
166Object.assign( Earth.ClassicEventDispatcher.prototype, {
167
168	addEventListener: function ( type, listener ) {
169
170		if ( this._listeners === undefined ) this._listeners = {};
171
172		const listeners = this._listeners;
173
174		if ( listeners[ type ] === undefined ) {
175
176			listeners[ type ] = [];
177
178		}
179
180		if ( listeners[ type ].indexOf( listener ) === - 1 ) {
181
182			listeners[ type ].push( listener );
183
184		}
185
186	},
187
188	hasEventListener: function ( type, listener ) {
189
190		if ( this._listeners === undefined ) return false;
191
192		const listeners = this._listeners;
193
194		return listeners[ type ] !== undefined && listeners[ type ].indexOf( listener ) !== - 1;
195
196	},
197
198	removeEventListener: function ( type, listener ) {
199
200		if ( this._listeners === undefined ) return;
201
202		const listeners = this._listeners;
203		const listenerArray = listeners[ type ];
204
205		if ( listenerArray !== undefined ) {
206
207			const index = listenerArray.indexOf( listener );
208
209			if ( index !== - 1 ) {
210
211				listenerArray.splice( index, 1 );
212
213			}
214
215		}
216
217	},
218
219	dispatchEvent: function ( event ) {
220
221		if ( this._listeners === undefined ) return;
222
223		const listeners = this._listeners;
224		const listenerArray = listeners[ event.type ];
225
226		if ( listenerArray !== undefined ) {
227
228			event.target = this;
229
230			// Make a copy, in case listeners are removed while iterating.
231			const array = listenerArray.slice( 0 );
232
233			for ( let i = 0, l = array.length; i < l; i ++ ) {
234
235				array[ i ].call( this, event );
236
237			}
238
239		}
240
241	}
242
243} );
244
245
246
247
248
249Object.assign( Earth.prototype, Earth.ClassicEventDispatcher.prototype );
250
251
252// private
253
254Earth.prototype.init = function() {
255	
256	this.clock = new THREE.Clock();
257	this.raycaster = new THREE.Raycaster();
258
259	// scene
260	this.scene = new THREE.Scene();
261	
262	// camera
263	this.camera = new THREE.PerspectiveCamera( 50, 1, 1, 2000 );
264	this.camera.position.z = Earth.camDistance;
265	this.scene.add( this.camera );
266	
267	// renderer
268	this.renderer = new THREE.WebGLRenderer( {
269		alpha: true,
270		antialias: true,
271		preserveDrawingBuffer: ( this.options.preserveDrawingBuffer ) ? true : false 
272	} );
273	
274	if ( this.options.shadows ) {
275		this.renderer.shadowMap.enabled = true;
276		this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
277	}
278	
279	this.renderer.setPixelRatio( window.devicePixelRatio );
280	
281	this.canvas = this.renderer.domElement;
282	this.element.appendChild( this.canvas );
283	
284	
285	// check capabilities for first instance on page
286	if ( ! Earth.capabilitiesChecked ) Earth.checkCapabilities( this.renderer );
287	
288
289	// earth
290	
291	this.sphere = new THREE.Mesh(
292		new THREE.SphereBufferGeometry( Earth.earthRadius, this.options.quality*16, this.options.quality*12 ),
293		new THREE.MeshBasicMaterial( { visible: false } )
294	);
295	
296	this.sphere.renderOrder = -2;
297	
298	if ( this.options.shadows ) {
299		this.sphere.receiveShadow = true;
300		this.sphere.castShadow = true;
301	}
302	
303	this.scene.add( this.sphere );
304	
305	
306	// inner earth
307	
308	if ( this.options.transparent ) {
309		this.innerSphere = new THREE.Mesh(
310			new THREE.SphereBufferGeometry( Earth.earthRadius, this.options.quality*16, this.options.quality*12 ), // - 0.001
311			new THREE.MeshBasicMaterial( { visible: false } )
312		);
313
314		this.innerSphere.renderOrder = -3;
315		this.sphere.add( this.innerSphere );
316	}
317	
318	
319	this.loadTexture();
320	
321	
322	// light
323	
324	this.ambientLight = new THREE.AmbientLight( 0xFFFFFF, this.options.lightAmbience );
325	this.scene.add( this.ambientLight );
326	
327	if ( this.options.light == 'simple' ) {
328		
329		this.primaryLight = new THREE.HemisphereLight( new THREE.Color(this.options.lightColor), new THREE.Color(this.options.lightGroundColor), this.options.lightIntensity );
330		this.scene.add( this.primaryLight );
331		
332	} else if ( this.options.light == 'sun' ) {
333		
334		this.primaryLight = new THREE.DirectionalLight( new THREE.Color(this.options.lightColor), this.options.lightIntensity );
335		
336		if ( this.options.shadows ) {
337			this.primaryLight.castShadow = true;
338			
339			this.primaryLight.shadow.mapSize.width =
340			this.primaryLight.shadow.mapSize.height = Earth.shadowSize[ this.options.quality ];
341			
342			var d = 25;
343			this.primaryLight.shadow.camera.left = -d;
344			this.primaryLight.shadow.camera.right = d;
345			this.primaryLight.shadow.camera.top = d;
346			this.primaryLight.shadow.camera.bottom = -d;
347			this.primaryLight.shadow.camera.far = 3000;
348			this.primaryLight.shadow.bias = 0.0001;
349		}
350		
351		this.scene.add( this.primaryLight );
352		
353	}
354	
355	
356	// events
357	
358	var dragStartMousePosition;
359	var ignoreClicks = false;
360	var lastTouchStart;
361	
362	var clickHandler = (function( event ){
363		
364		if ( this.paused ) return;
365		
366		if ( ignoreClicks && event.type == 'click' ) return;
367		
368		
369		// touch to mousevents
370		
371		var touchEndClick = false;
372		
373		if ( event.type == 'touchstart' ||  event.type == 'touchend' ) {
374		
375			if ( event.touches.length > 1 ) return;
376
377			var x = event.changedTouches[0].clientX, y = event.changedTouches[0].clientY;
378		
379			if ( event.type == 'touchstart' ) {
380				
381				var type = 'mousedown';
382				lastTouchStart = new THREE.Vector2( x, y );
383				
384			} else if ( event.type == 'touchend' ) {
385				
386				var type = 'mouseup';
387				touchEndClick = ! ignoreClicks && lastTouchStart && lastTouchStart.distanceTo( new THREE.Vector2( x, y ) ) < Earth.maxClickDistance;
388				
389			}
390			
391		} else {
392			
393			var type = event.type;
394			var x = event.clientX, y = event.clientY;
395			
396		}
397		
398
399		var mouse = Earth.normalizeMouse( this, x, y );
400		var intersects = this.raycast( mouse );
401		var intersect_sphere = false;
402		
403		for ( var i = 0; i < intersects.length; i++ ) {
404			
405			if ( intersects[ i ].object == this.sphere ) {
406				var uv = intersects[ i ].uv;
407				intersects[ i ].object.material.map.transformUv( uv );
408				
409				var mouseEvent = { type: type, x : mouse.x, y : mouse.y, location : Earth.uvToLatLng( uv ), originalEvent: event };
410				
411				if ( this.mapHitTest ) {
412					mouseEvent.id = this.hitTestMap( uv );
413				}
414				
415				this.dispatchEvent( mouseEvent );
416				
417				if ( touchEndClick ) {
418					mouseEvent.type = 'click';
419					this.dispatchEvent( mouseEvent );
420				}
421				
422				intersect_sphere = true;
423
424			} else if ( intersects[ i ].object.userData.marker && intersects[ i ].object.userData.marker.hotspot ) {
425				
426				if ( ! intersect_sphere || (intersects[ i ].object.userData.marker.isSprite && ! intersects[ i ].object.userData.marker.occluded) ) {
427
428					if ( type == 'mousedown' && Earth.hasEvent( intersects[ i ].object.userData.marker, ['mousedown'] ) ) {
429						intersects[ i ].object.userData.marker.dispatchEvent( { type: "mousedown", originalEvent: event } );
430						event.stopImmediatePropagation();
431						event.preventDefault();
432						break;
433						
434					} else if ( type == 'mouseup' && Earth.hasEvent( intersects[ i ].object.userData.marker, ['mouseup'] ) ) {
435						intersects[ i ].object.userData.marker.dispatchEvent( { type: "mouseup" } );
436						if ( touchEndClick ) {
437							intersects[ i ].object.userData.marker.dispatchEvent( { type: "click", originalEvent: event } );
438						}
439						event.stopImmediatePropagation();
440						event.preventDefault();
441						break;
442						
443					} else if ( ( type == 'click' || touchEndClick ) && Earth.hasEvent( intersects[ i ].object.userData.marker, ['click'] ) ) {
444						intersects[ i ].object.userData.marker.dispatchEvent( { type: "click", originalEvent: event } );
445						event.stopImmediatePropagation();
446						event.preventDefault();
447						break;
448						
449					}
450				
451				}
452
453				
454			}
455			
456		}
457
458	}).bind( this );
459	
460	this.canvas.addEventListener( 'click', clickHandler );
461	this.canvas.addEventListener( 'mousedown', clickHandler );
462	this.canvas.addEventListener( 'mouseup', clickHandler );
463	this.canvas.addEventListener( 'touchstart', clickHandler );
464	this.canvas.addEventListener( 'touchend', clickHandler );
465		
466	this.canvas.addEventListener( 'mouseover', (function(event){
467		
468		if ( this.paused ) return;
469		this.mouseOver = true;
470		
471	}).bind( this ) );
472	
473	this.canvas.addEventListener( 'mouseout', (function(event){
474		
475		if ( this.paused ) return;
476		this.mouseOver = false;
477		this.mouseOverEarth = false;
478		
479		if ( this.mouseOverObject ) {
480			this.mouseOverObject.dispatchEvent( { type: "mouseout", originalEvent: event } );
481			this.mouseOverObject = null;
482		}
483		
484	}).bind( this ) );	
485	
486	this.canvas.addEventListener( 'mousemove', (function(event) {
487		
488		if ( this.paused ) return;
489		this.mousePosition = Earth.normalizeMouse( this, event.clientX, event.clientY );
490	
491	}).bind( this ) );
492	
493	this.canvas.addEventListener( 'touchmove', (function(event) {
494		
495		if ( this.paused ) return;
496		this.mousePosition = Earth.normalizeMouse( this, event.touches[0].clientX, event.touches[0].clientY );
497
498	}).bind( this ) );
499	
500	
501	
502	document.addEventListener( 'mousemove', (function(event) {
503		
504		if ( this.paused ) return;
505		this.docMousePosition = Earth.getEventPosition( event );
506	
507	}).bind( this ) );
508	
509	document.body.addEventListener( 'touchmove', (function(event) {
510		
511		if ( this.paused ) return;
512		this.docMousePosition = Earth.getEventPosition( event );
513	
514	}).bind( this ) );
515	
516	
517	
518	// orbit
519	
520	this.orbit = new Earth.Orbit( this.camera, this );
521	
522	this.orbit.addEventListener( "start", (function() {
523		
524		this.dragging = true;
525		if ( this.grabCursor ) document.documentElement.classList.add( 'earth-dragging' );
526		
527		if ( this.goAnimation ) {
528			this.goAnimation.stop();
529		}
530		
531		if ( this.ready ) this.dispatchEvent( { type: 'dragstart' } );
532		
533		// save mouse position
534		dragStartMousePosition = this.docMousePosition;
535		
536	}).bind( this ) );
537	
538	this.orbit.addEventListener( "change", (function( event ) {
539			
540		if ( this.ready ) this.dispatchEvent( { type: 'change' } );
541		
542		this.radius = this.getRadius();
543		
544		// beginn to ignore clicks if dragged a little distance
545		if ( this.dragging && ! ignoreClicks ) {
546			ignoreClicks = dragStartMousePosition && dragStartMousePosition.distanceTo( this.docMousePosition ) > Earth.maxClickDistance;
547		}
548
549	}).bind( this ) );
550
551	this.orbit.addEventListener( "end", (function() {
552		
553		this.momentum.copy( this.mouseVelocity );
554		this.mouseVelocity.set(0, 0);
555		this.lastDocMousePosition = false;
556		
557		document.documentElement.classList.remove( 'earth-dragging' );
558		
559		if ( this.dragging ) {
560			this.dragging = false;
561			if ( this.ready ) this.dispatchEvent( { type: 'dragend' } );
562		}
563		
564		// enable clicks after dragend
565		if ( ignoreClicks ) {
566			setTimeout( function(){ ignoreClicks = false; }, 1 );
567		}
568		
569	}).bind( this ) );
570	
571	
572	// set properties
573	
574	Object.assign( this, this.options );
575	
576	this.update();
577
578	setTimeout( (function() {
579		this.ready = true;
580		this.element.classList.add( 'earth-ready' );
581		this.dispatchEvent( { type: 'ready' } );
582		this.dispatchEvent( { type: 'change' } );
583	}).bind( this ), 1 );
584	
585};
586
587
588// PUBLIC
589// properties
590
591Object.defineProperties( Earth.prototype, {
592
593	location : {
594		get: function() {
595			
596			return Earth.worldToLatLng( this.camera.position );			
597			
598		},
599		set: function( v ) {
600			var latlng = Object.assign( {}, v );
601			latlng = Earth.formatLatLng( latlng );
602			
603			// limit lat
604			if ( ! this.goAnimation ) { // no limits during rotation animation
605				latlng.lat = Math.min( this.maxLat(), Math.max( this.minLat(), latlng.lat ) );
606			}
607			
608			this.orbit.setPosition( Earth.latLngToWorld( latlng, this.camera.position.length() ) );
609			this.resetAutoRotate();
610			
611		}
612	},
613	
614	paused : {
615		get: function() {
616			return this.options.paused;
617		},
618		set: function( v ) {
619			if ( v ) {
620				this.options.paused = true;
621				
622			} else if ( this.options.paused ) { // resume if paused
623				this.options.paused = false;
624				
625				if ( this.ready ) { // if not ready, update will be called when ready
626					this.update();
627				}
628				
629			}
630		}
631	},
632	
633	mapLandColor : {
634		get: function() {
635			return this.options.mapLandColor;
636		},
637		set: function( v ) {
638			this.options.mapLandColor = v;
639		}
640	},	
641	mapSeaColor : {
642		get: function() {
643			return this.options.mapSeaColor;
644		},
645		set: function( v ) {
646			this.options.mapSeaColor = v;
647		}
648	},
649	mapBorderColor : {
650		get: function() {
651			return this.options.mapBorderColor;
652		},
653		set: function( v ) {
654			this.options.mapBorderColor = v;
655		}
656	},
657	mapBorderWidth : {
658		get: function() {
659			return this.options.mapBorderWidth;
660		},
661		set: function( v ) {
662			this.options.mapBorderWidth = v;
663		}
664	},
665	mapStyles : {
666		get: function() {
667			return this.options.mapStyles;
668		},
669		set: function( v ) {
670			this.options.mapStyles = v;
671		}
672	},
673	mapSvg : {
674		get: function() {
675			return this.options.mapSvg;
676		},
677		set: function( v ) {
678			this.options.mapSvg = v;
679			this.removeHitTester();
680		}
681	},
682	mapImage : {
683		get: function() {
684			return this.options.mapImage;
685		},
686		set: function( v ) {
687			this.options.mapImage = v;
688		}
689	},
690	
691	draggable : {
692		get: function() {
693			return this.orbit.enableRotate;
694		},
695		set: function( v ) {
696			this.orbit.enableRotate = v;
697			
698			if ( ! v && this.dragging ) { // stop dragging
699				this.orbit.cancel();
700				//this.orbit.enabled = false;
701			}
702			
703		}
704	},
705
706	dragPolarLimit : {
707		get: function() {
708			return this.options.dragPolarLimit;
709		},
710		set: function( v ) {
711			this.options.dragPolarLimit = Math.max( 0, Math.min( 1, v ) );
712			this.orbit.minPolarAngle = this.options.dragPolarLimit/2 * Math.PI; // radians
713			this.orbit.maxPolarAngle = (1-this.options.dragPolarLimit/2) * Math.PI; // radians
714		}
715	},
716	
717	polarLimit : {
718		get: function() {
719			return this.options.polarLimit;
720		},
721		set: function( v ) {
722			this.options.polarLimit = Math.max( 0, Math.min( 1, v ) );
723		}
724	},
725	
726	
727	autoRotate : {
728		get: function() {
729			return this.options.autoRotate;
730		},
731		set: function( v ) {
732			this.options.autoRotate = v;
733			if ( ! v ) {
734				this.resetAutoRotate();
735			}
736		}
737	},
738
739
740	zoom : {
741		get: function() {
742			return Earth.camDistance / this.camera.position.length();
743		},
744		set: function( v ) {
745			this.orbit.setPosition( this.camera.position.normalize().multiplyScalar( Earth.camDistance / v ) );
746		}
747	},
748	zoomable : {
749		get: function() {
750			return this.orbit.enableZoom;
751		},
752		set: function( v ) {
753			this.orbit.enableZoom = v;
754		}
755	},
756	zoomMin : {
757		get: function() {
758			return 1 / (this.orbit.maxDistance / Earth.camDistance);
759		},
760		set: function( v ) {
761			this.orbit.maxDistance = Earth.camDistance * (1/v);
762		}
763	},
764	zoomMax : {
765		get: function() {
766			return 1 / (this.orbit.minDistance / Earth.camDistance);
767		},
768		set: function( v ) {
769			this.orbit.minDistance = Earth.camDistance * (1/v);
770		}
771	},
772	zoomSpeed : {
773		get: function() {
774			return this.orbit.zoomSpeed;
775		},
776		set: function( v ) {
777			this.orbit.zoomSpeed = v;
778		}
779	},
780	
781	
782	lightAmbience : {
783		get: function() {
784			return this.ambientLight.intensity;
785		},
786		set: function( v ) {
787			this.ambientLight.intensity = v;
788		}
789	},
790	
791	lightIntensity : {
792		get: function() {
793			if ( ! this.primaryLight ) return 1;
794			return this.primaryLight.intensity;
795		},
796		set: function( v ) {
797			if ( ! this.primaryLight ) return;
798			this.primaryLight.intensity = v;
799		}
800	},	
801	lightColor : {
802		get: function() {
803			if ( ! this.primaryLight ) return '#FFFFFF';
804			return '#' + this.primaryLight.color.getHexString();
805		},
806		set: function( v ) {
807			if ( ! this.primaryLight ) return;
808			this.primaryLight.color = new THREE.Color(v);
809		}
810	},
811	lightGroundColor : {
812		get: function() {
813			if ( ! this.primaryLight || ! this.primaryLight.isHemisphereLight ) return '#FFFFFF';
814			return '#' + this.primaryLight.groundColor.getHexString();
815		},
816		set: function( v ) {
817			if ( ! this.primaryLight || ! this.primaryLight.isHemisphereLight ) return;
818			this.primaryLight.groundColor = new THREE.Color(v);
819		}
820	},
821	
822	sunLocation : {
823		get: function() {
824			if ( ! this.primaryLight || ! this.primaryLight.isDirectionalLight ) return { lat: 0, lng: 0 };
825			return Earth.worldToLatLng( this.primaryLight.position );	
826		},
827		set: function( v ) {
828			if ( ! this.primaryLight || ! this.primaryLight.isDirectionalLight ) return;
829			this.primaryLight.position.copy( Earth.latLngToWorld( Earth.formatLatLng(v), Earth.camDistance ) );
830		}
831	},
832	
833	sunDirection : {
834		get: function() {
835			if ( ! this.primaryLight || ! this.primaryLight.isDirectionalLight ) return false;
836			return this.options.sunDirection;	
837		},
838		set: function( v ) {
839			if ( ! this.primaryLight || ! this.primaryLight.isDirectionalLight ) return;
840			if ( ! v || typeof v.x == 'undefined' ) return;
841			if ( this.primaryLight.parent !== this.camera ) {
842				this.camera.add( this.primaryLight );
843			}			
844			this.primaryLight.position.set( v.x*50, v.y*50, 0);
845		}
846	},
847	
848
849} );
850
851
852// private
853// determine quality based on container size
854
855Earth.prototype.getQuality = function() {
856	
857	return ( this.element.offsetWidth >= 720 ) ? 4 : 3;
858	
859};
860
861
862// private
863// get min and max latitude according to polarLimit
864
865Earth.prototype.minLat = function() {
866	return (1 - this.options.polarLimit) * -90;
867};
868Earth.prototype.maxLat = function() {
869	return (1 - this.options.polarLimit) * 90;	
870};
871
872
873
874// private
875// handle autoRotate
876
877Earth.prototype.updateAutoRotate = function() {
878	
879	this.autoRotateTime += this.deltaTime;
880	
881	if ( this.autoRotateTime > this.autoRotateDelay ) {
882		
883		if ( ! this.autoRotate ) { // start autoRotate
884			this.dispatchEvent( { type: 'autorotate' } );
885			this.autoRotate = true;
886		}
887		
888		var t = ( this.autoRotateTime - this.autoRotateDelay ) / this.autoRotateStart;
889		
890		if ( t > 1 ) {
891			this.orbit.autoRotateSpeed = this.autoRotateSpeed;
892			this.orbit.autoRotateSpeedUp = this.autoRotateSpeedUp;
893		} else {
894			this.orbit.autoRotateSpeed = THREE.Math.lerp( 0, this.autoRotateSpeed, Earth.Animation.Easing[ this.autoRotateEasing ](t) );
895			this.orbit.autoRotateSpeedUp = THREE.Math.lerp( 0, this.autoRotateSpeedUp, Earth.Animation.Easing[ this.autoRotateEasing ](t) );
896		}
897	
898	} else {
899
900		this.orbit.autoRotateSpeed = 0;
901		this.orbit.autoRotateSpeedUp = 0;
902	
903	}
904	
905};
906
907
908// PUBLIC
909// start autoRotate without delay
910
911Earth.prototype.startAutoRotate = function( easeIn ) {
912	this.autoRotateTime = this.autoRotateDelay + ( ( easeIn ) ? 0 : this.autoRotateStart );
913	this.autoRotate = true;
914	this.autoRotating = true;
915};
916
917// PUBLIC
918// interrupt rotation
919
920Earth.prototype.resetAutoRotate = function() {
921	this.autoRotateTime = 0;
922	this.autoRotating = false;
923};
924
925
926// private
927// handle momentum
928
929Earth.prototype.updateMomentum = function() {
930	
931	if ( this.dragging ) {
932		
933		this.resetAutoRotate();
934		this.orbit.autoRotateSpeed = 0;
935		this.orbit.autoRotateSpeedUp = 0;
936		this.momentum.set( 0, 0 );
937		
938		return;
939		
940	}
941	
942	if ( this.momentum.equals( Earth.zeroMomentum ) ) {
943		
944		if ( this.autoRotate ) {
945			
946			this.updateAutoRotate();
947			
948		} else {
949			
950			this.orbit.autoRotateSpeed = 0;
951			this.orbit.autoRotateSpeedUp = 0;
952			
953		}
954		
955		return;
956		
957	}
958	
959	this.resetAutoRotate();
960	
961	this.orbit.autoRotateSpeed = this.momentum.x * 10000;
962	this.orbit.autoRotateSpeedUp = this.momentum.y * 10000;
963	
964	this.momentum.set(
965		THREE.Math.lerp( this.momentum.x, 0, this.deltaTime / (2000 - (this.dragDamping*1999)) ),
966		THREE.Math.lerp( this.momentum.y, 0, this.deltaTime / (2000 - (this.dragDamping*1999)) )
967	);
968	
969	
970	if ( Math.abs(this.momentum.x) < 0.00005 ) {
971		this.momentum.x = 0;
972	}
973	if ( Math.abs(this.momentum.y) < 0.00005 ) {
974		this.momentum.y = 0;
975	}	
976	
977};
978
979
980// private
981// calculate velocity for momentum
982
983Earth.prototype.updatePointerVelocity = function() {
984
985	if ( this.dragging && this.dragMomentum && this.docMousePosition && this.lastDocMousePosition ) {
986		
987		var moveVelocity = this.docMousePosition.clone().sub(
988			this.lastDocMousePosition
989		).multiplyScalar( 0.00005 );
990		
991		var max_velocity_x = 0.002;
992		var max_velocity_y = 0.0015;
993		// limit velocity
994		if ( moveVelocity.x < -max_velocity_x ) moveVelocity.x = -max_velocity_x;
995		else if ( moveVelocity.x > max_velocity_x ) moveVelocity.x = max_velocity_x;
996		if ( moveVelocity.y < -max_velocity_y ) moveVelocity.y = -max_velocity_y;
997		else if ( moveVelocity.y > max_velocity_y ) moveVelocity.y = max_velocity_y;
998		
999		
1000		//this.mouseVelocity.lerp( moveVelocity, Math.min(1, this.deltaTime/50) );
1001		this.mouseVelocity.copy( moveVelocity );
1002		
1003	} else {
1004		
1005		this.mouseVelocity.set( 0, 0 );
1006		
1007	}
1008	
1009	this.lastDocMousePosition = ( this.docMousePosition ) ? this.docMousePosition.clone() : false;
1010	
1011};
1012
1013
1014// private
1015// handle mouse over/out events and cursors
1016
1017Earth.prototype.updatePointer = function() {
1018
1019	// mouse over / out
1020	
1021	var overObject = false;
1022	this.mouseOverEarth = false;
1023	
1024	
1025	if ( this.mouseOver && ! this.dragging ) {
1026	
1027		var intersects = this.raycast( this.mousePosition );
1028		var intersect_sphere = false;
1029		
1030		for ( var i = 0; i < intersects.length; i++ ) {
1031			
1032			var obj = intersects[ i ].object;
1033			
1034			if ( obj == this.sphere ) {
1035				this.mouseOverEarth = true;
1036				intersect_sphere = true;
1037			
1038			} else if ( ! overObject && obj.userData.marker && obj.userData.marker.hotspot && Earth.hasEvent( obj.userData.marker, ['click','mousedown','mouseup','mouseover','mouseout'] ) ) {
1039				
1040				if ( ! intersect_sphere || (intersects[ i ].object.userData.marker.isSprite && ! intersects[ i ].object.userData.marker.occluded) ) {
1041					overObject = intersects[ i ].object.userData.marker;
1042				}
1043				
1044			}
1045		}
1046		
1047	}
1048	
1049	
1050	if ( overObject ) {
1051		
1052		if ( this.mouseOverObject && this.mouseOverObject != overObject ) {
1053			this.mouseOverObject.dispatchEvent( { type: "mouseout" } );
1054		}
1055		
1056		this.mouseOverObject = overObject;
1057		this.mouseOverObject.dispatchEvent( { type: "mouseover" } ); 
1058		
1059	} else if ( this.mouseOverObject ) {
1060		
1061		this.mouseOverObject.dispatchEvent( { type: "mouseout" } );
1062		this.mouseOverObject = null;
1063		
1064	}
1065	
1066	
1067	if ( this.mouseOverObject && Earth.hasEvent( this.mouseOverObject, ['click'] ) ) {
1068		this.element.classList.add( 'earth-clickable' );
1069	} else {
1070		this.element.classList.remove( 'earth-clickable' );
1071	}
1072	
1073	
1074	if ( this.draggable && this.grabCursor && this.mouseOverEarth ) {
1075		this.element.classList.add( 'earth-draggable' );
1076	} else {
1077		this.element.classList.remove( 'earth-draggable' );
1078	}
1079	
1080};
1081
1082
1083
1084// PUBLIC
1085
1086Earth.prototype.hitTest = function( latlng ) {
1087	
1088	return this.hitTestMap( Earth.latLngToUv( latlng ) );
1089	
1090};
1091
1092// private
1093// hit test a uv position and return path id
1094
1095Earth.prototype.hitTestMap = function( uv ) {
1096	
1097	if ( ! this.mapSvg ) return '';
1098	
1099	if ( ! this.hitTester ) {
1100		this.hitTester = document.createElement( 'div' );
1101		document.body.appendChild( this.hitTester );
1102		this.hitTester.className = 'earth-hittest';
1103		this.hitTester.innerHTML = String( this.mapSvg ).replace(/\sid\s*=/gi, ' data-id=');  // escape IDs
1104	}
1105	
1106	var svgElement = this.hitTester.querySelector('svg');
1107	if ( ! svgElement ) return '';
1108	
1109	this.hitTester.style.display = 'block';
1110	
1111	var rect = svgElement.getBoundingClientRect();
1112	var elem = document.elementFromPoint( uv.x * rect.width, uv.y * rect.height );
1113	
1114	this.hitTester.style.display = 'none';
1115
1116	if ( elem && elem.dataset && elem.dataset.id ) {
1117		return elem.dataset.id;
1118	} else {
1119		return '';
1120	}
1121	
1122};
1123
1124// private
1125// removes hitTester if earth is destroyed or mapSvg is changed
1126
1127Earth.prototype.removeHitTester = function() {
1128	if ( this.hitTester ) {
1129		this.hitTester.remove();
1130		this.hitTester = null;
1131	}
1132};
1133
1134
1135// private
1136// raycast of mouse position
1137
1138Earth.prototype.raycast = function( mouse ) {
1139	
1140	this.raycaster.setFromCamera( Earth.normalizeRaycast(mouse), this.camera );
1141	return this.raycaster.intersectObjects( this.scene.children );
1142	
1143};
1144
1145
1146// PUBLIC
1147// call redrawMap if map properties have been changed
1148
1149Earth.prototype.redrawMap = function() {
1150	
1151	this.loadTexture();
1152	
1153};
1154
1155
1156// PUBLIC
1157// updates the map texture after drawing to the mapCanvas
1158
1159Earth.prototype.updateMap = function() {
1160	
1161	if ( this.mapTexture ) this.mapTexture.needsUpdate = true;
1162	
1163};
1164
1165
1166// private
1167// is svg?
1168
1169Earth.prototype.mapImageIsSvg = function( url ) {
1170	
1171	return String(url).toLowerCase().indexOf('.svg') != -1;
1172	
1173};
1174
1175
1176
1177// private
1178// load map texture
1179
1180Earth.prototype.loadTexture = function( useMapSvg, isDefaultMap ) {
1181	
1182	if ( useMapSvg ) { // svg map
1183		
1184		var svg = this.mapSvg;
1185		
1186		if ( isDefaultMap ) {
1187			var style = '<style type="text/css">';
1188			style += '#SEA { fill:'+ this.options.mapSeaColor +'; }';
1189			style += 'path { fill:'+ this.options.mapLandColor +'; ';
1190			style += 'stroke:'+ ( this.options.mapBorderColor ? this.options.mapBorderColor : this.options.mapLandColor ) +'; ';
1191			style += 'stroke-width:'+ this.options.mapBorderWidth +'; stroke-miterlimit:1; }';
1192			style += this.options.mapStyles;
1193			style += '</style>';
1194			svg = svg.replace( /(<svg[^>]+>)/i, '$1 ' + style );
1195		}
1196		
1197		this.mapImageElem = document.createElement("img");
1198		this.mapImageElem.setAttribute( "src", "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg))) );
1199		this.mapImageElem.onload = this.drawTexture.bind( this );
1200		
1201		
1202	} else if ( this.options.legacySupportIE11 && Earth.isIE11() && this.options.fallbackMapUrlIE11 ) { // IE fallback map image
1203	
1204		this.mapImageElem = document.createElement("img");
1205		this.mapImageElem.setAttribute( "src", this.options.fallbackMapUrlIE11 );
1206		this.mapImageElem.onload = this.drawTexture.bind( this );
1207		
1208		
1209	} else if ( this.options.mapImage && ! this.mapImageIsSvg( this.options.mapImage ) ) { // pixel image
1210	
1211		this.mapImageElem = document.createElement("img");
1212		this.mapImageElem.setAttribute( "src", this.options.mapImage );
1213		this.mapImageElem.onload = this.drawTexture.bind( this );
1214		
1215		
1216	} else if ( this.options.mapImage ) { // custom svg
1217	
1218		var thisEarth = this;
1219		var ajax = new XMLHttpRequest();
1220
1221		ajax.onreadystatechange = function() {
1222			if ( this.readyState == 4 ) {
1223				thisEarth.mapSvg = Earth.fixSvgNamespace( this.responseText );
1224				thisEarth.loadTexture( true, false );
1225			}
1226		};
1227		
1228		ajax.open("GET", this.options.mapImage, true);
1229		ajax.send();
1230	
1231	
1232	} else if ( Earth.mapSvg ) { // default svg
1233		
1234		this.mapSvg = Earth.fixSvgNamespace( Earth.mapSvg );
1235		this.loadTexture( true, true );
1236		
1237		
1238	} else {
1239		
1240		setTimeout( this.drawTexture.bind( this ), 1 );
1241		
1242		
1243	}
1244	
1245};
1246
1247
1248// private
1249// draw the map texture, called by loadTexture
1250
1251Earth.prototype.drawTexture = function() {
1252	
1253	if ( ! this.mapCanvas ) {
1254		this.mapCanvas = document.createElement( "canvas" );
1255		this.mapCanvas.width = Earth.textureSize[ this.quality ];
1256		this.mapCanvas.height = Earth.textureSize[ this.quality ] / 2;
1257		this.mapContext = this.mapCanvas.getContext("2d");
1258		
1259	} else { // already exits
1260		this.mapContext.clearRect(0, 0, this.mapCanvas.width, this.mapCanvas.height);
1261		
1262	}
1263	
1264	
1265	// draw map image
1266	
1267	if ( this.mapImageElem ) {
1268		this.mapContext.drawImage( this.mapImageElem, 0, 0, this.mapImageElem.width, this.mapImageElem.height, 0, 0, this.mapCanvas.width, this.mapCanvas.height );
1269	}
1270		
1271	this.dispatchEvent( { type: 'drawtexture', canvas: this.mapCanvas, context : this.mapContext } );
1272	
1273	
1274	if ( ! this.mapTexture ) {
1275		
1276		this.mapTexture = new THREE.CanvasTexture( this.mapCanvas );
1277		this.mapTexture.wrapS = THREE.RepeatWrapping; // this.mapTexture.wrapT = 
1278		this.mapTexture.anisotropy = Earth.anisotropy[ this.quality ];
1279		this.mapTexture.offset = new THREE.Vector2( -0.25, 0 );
1280	
1281		var earthMaterial = {
1282			map: this.mapTexture,
1283			shininess: this.shininess * 100,
1284			dithering : (this.light != 'none')
1285		};
1286		
1287		// inner earth
1288		if ( this.transparent ) {
1289			
1290			earthMaterial.transparent = true;
1291			earthMaterial.depthWrite = false;
1292
1293			this.innerSphere.material = new THREE.MeshBasicMaterial( {
1294				dithering : (this.light != 'none'),
1295				transparent : true,
1296				color : this.innerColor,
1297				opacity : this.innerOpacity,
1298				side: THREE.BackSide,
1299				depthWrite : false,
1300				map: this.mapTexture
1301			} );
1302
1303		}
1304
1305		this.sphere.material = new THREE.MeshPhongMaterial( earthMaterial );
1306	
1307	
1308	} else { // already exists
1309	
1310		this.mapTexture.needsUpdate = true;
1311		
1312	}
1313	
1314};
1315
1316
1317// private
1318// watch for bounding rect changes
1319
1320Earth.prototype.updateBounds = function() {
1321	
1322	this.bounds = this.element.getBoundingClientRect();
1323	
1324	this.outOfView = 	this.bounds.right < 0 || this.bounds.bottom < 0 ||
1325						this.bounds.left > window.innerWidth  || this.bounds.top > window.innerHeight;
1326						
1327	this.isVisible = this.bounds.width && this.bounds.height;
1328	
1329	
1330	if ( this.elementSize.x == this.bounds.width && this.elementSize.y == this.bounds.height ) return; // not resized
1331	
1332	this.elementSize.set( this.bounds.width, this.bounds.height );
1333	this.elementCenter.set( this.bounds.width / 2, this.bounds.height / 2 );
1334	this.containerScale = Math.min( this.elementSize.x, this.elementSize.y ) / 1000; // for overlay scaling
1335	
1336	this.camera.aspect = this.bounds.width / this.bounds.height;
1337	this.camera.updateProjectionMatrix();
1338	this.renderer.setSize( this.bounds.width, this.bounds.height );
1339	
1340	this.radius = this.getRadius();
1341	
1342};
1343
1344
1345// private
1346// main loop
1347
1348Earth.prototype.update = function() {
1349	
1350	if ( this.paused ) return;
1351	
1352	requestAnimationFrame( this.update.bind(this) );
1353	
1354	if ( ! this.ready ) return;
1355	
1356	this.deltaTime = Math.min( 100, this.clock.getDelta() * 1000 );
1357	
1358	this.updateBounds();
1359	this.updatePointerVelocity();
1360	this.updatePointer();
1361	this.updateMomentum();
1362	this.orbit.update();
1363
1364	/* >>> */
1365	this.updateAnimations();
1366	/* <<< */
1367
1368	this.dispatchEvent( { type: "update" } );
1369
1370
1371	if ( ! this.outOfView && this.isVisible ) {		
1372
1373		this.renderer.render( this.scene, this.camera );
1374		
1375		/* >>> */
1376		this.updateOccludables();
1377		/* <<< */
1378		
1379		this.updateOverlays();	
1380	
1381	}
1382	
1383};
1384
1385
1386
1387// PUBLIC
1388// create and add objects
1389
1390Earth.prototype.addMarker = function( options ) {
1391	return new Earth.Marker( options, this );
1392};
1393
1394/* >>> */
1395
1396Earth.prototype.addImage = function( options ) {
1397	return new Earth.Image( options, this );
1398};
1399
1400
1401Earth.prototype.addSprite = function( options ) {
1402	return new Earth.Sprite( options, this );
1403};
1404
1405
1406Earth.prototype.addPoints = function( options ) {
1407	return new Earth.Points( options, this );
1408};
1409
1410
1411Earth.prototype.addLine = function( options ) {
1412	return new Earth.Line( options, this );
1413};
1414
1415/* <<< */
1416
1417Earth.prototype.addOverlay = function( options ) {
1418	return new Earth.Overlay( options, this );
1419};
1420
1421
1422// private
1423// handle overlays
1424
1425Earth.prototype.updateOverlays = function() {
1426	
1427	var updateOrder = false;
1428	var camDistance = this.camera.position.length();
1429	
1430	for (var i=0; i < this.overlays.length; i++) {
1431		
1432		var overlay = this.overlays[i];
1433		
1434		
1435		updateOrder = true;
1436	
1437		// position
1438		overlay.updatePositions();
1439		
1440		if ( overlay.elementPosition.y < overlay.earth.elementSize.y / 2 ) {
1441			overlay.element.classList.add('earth-overlay-top');
1442		} else {
1443			overlay.element.classList.remove('earth-overlay-top');
1444		}
1445		
1446		if ( overlay.elementPosition.x < overlay.earth.elementSize.x / 2 ) {
1447			overlay.element.classList.add('earth-overlay-left');
1448		} else {
1449			overlay.element.classList.remove('earth-overlay-left');
1450		}
1451		
1452		// occlusion
1453		overlay.updateOcclusion();
1454		
1455		if ( overlay.occluded ) {
1456			overlay.element.classList.add('earth-occluded');
1457		} else {
1458			overlay.element.classList.remove('earth-occluded');
1459		}
1460		
1461		
1462		if ( ! overlay.visible ) continue;
1463		
1464		
1465		// scaling
1466		var scale = 1;
1467		
1468		if ( overlay.depthScale ) {
1469			scale *= Math.max( 0, 1 - overlay.depthScale + (camDistance-overlay.distance) / Earth.earthRadius * overlay.depthScale );
1470		}		
1471		if ( overlay.zoomScale ) {
1472			scale *= THREE.Math.lerp( 1, this.zoom, overlay.zoomScale );
1473		}
1474		if ( overlay.containerScale ) {
1475			scale *= THREE.Math.lerp( 1, this.containerScale, overlay.containerScale );
1476		}
1477		
1478		scale = Math.max(0, scale);
1479		var scaleTransform = ( scale!=1 ) ? ' scale('+ scale +')' : '';
1480		
1481		
1482		// apply transform
1483		overlay.element.style.transform = 'translate('+ overlay.elementPosition.x +'px, '+ overlay.elementPosition.y +'px)' + scaleTransform;
1484		
1485		
1486		if ( ! overlay.ready ) {
1487			overlay.ready = true;
1488			overlay.visible = true;
1489		}
1490	
1491	}
1492	
1493	
1494	if ( updateOrder ) {
1495	
1496		// sort by cam distance for z-index
1497		
1498		this.overlays.sort( function(a, b) {
1499			return b.distance - a.distance;
1500		} );
1501	
1502		for (var i=0; i < this.overlays.length; i++) {
1503			this.overlays[i].element.style.zIndex = (( this.overlays[i].occluded && this.overlays[i].occlude ) ? 10 : 1010) + i;
1504		}
1505		
1506	}
1507	
1508};
1509
1510
1511// private
1512// handle occlusion
1513
1514Earth.prototype.updateOccludables = function() {
1515	
1516	for ( var i=0; i < this.occludables.length; i++ ) {
1517		
1518		var occludable = this.occludables[i];
1519		
1520		occludable.updatePositions();
1521		occludable.updateOcclusion();
1522
1523	}
1524	
1525};
1526
1527
1528// private
1529// handle all animations on this earth
1530/* >>> */
1531
1532Earth.prototype.updateAnimations = function() {
1533	
1534	var complete_animations = [];
1535	
1536	for ( var i in this.animations ) {
1537		var ani = this.animations[i];
1538		
1539		if ( ani.paused ) continue;
1540		
1541		if ( ani.target.removed ) {
1542			complete_animations.push( ani );
1543			continue;
1544		}
1545		
1546		ani.time += this.deltaTime;
1547		var p = ani.time / ani.duration; // progress 0-1
1548		
1549		ani.step( Math.min( p, 1 ) );
1550		
1551		if ( p >= 1 ) { // end of animation
1552		
1553			if ( ani.loop ) {
1554				
1555				if ( ani.oscillate ) {
1556					ani.time = Math.max( 0, ani.duration - ani.time );
1557					var current = ani.to;
1558					ani.to = ani.from;
1559					ani.from = current;
1560				} else {
1561					ani.time = Math.max( 0, ani.duration - ani.time );
1562				}
1563				
1564				ani.dispatchEvents( true );
1565				
1566			} else {
1567				complete_animations.push( ani );
1568			}
1569			
1570		}
1571		
1572	}
1573	
1574	for ( var i in complete_animations ) {
1575		complete_animations[i].stop( true );
1576	}
1577	
1578};
1579
1580/* <<< */
1581
1582
1583
1584// PUBLIC
1585// animate earth location and zoom
1586// special options: zoom, approachAngle
1587// returns false or aniInstance
1588
1589Earth.prototype.goTo = function( location, options ) {
1590	
1591	if ( ! options ) options = {};
1592	
1593	if ( options.approachAngle ) {
1594	
1595		var toPos = Earth.latLngToWorld( location, Earth.earthRadius );
1596		var camPos = this.camera.position.clone().normalize().multiplyScalar( Earth.earthRadius );
1597		
1598		if ( THREE.Math.radToDeg( toPos.angleTo( camPos ) ) > options.approachAngle ) {
1599			
1600			for ( var i=1; i <= 32; i++ ) {
1601				
1602				var midPos = new THREE.Vector3().lerpVectors( camPos, toPos, i/32 ).normalize().multiplyScalar( Earth.earthRadius );
1603				
1604				if ( THREE.Math.radToDeg( toPos.angleTo( midPos ) ) <= options.approachAngle ) {
1605					
1606					location = Earth.worldToLatLng( midPos );
1607					break;
1608					
1609				}
1610				
1611			}
1612			
1613		} else {
1614		
1615			return false;
1616			
1617		}
1618		
1619	}
1620	
1621	
1622	// cancel previous animation
1623	
1624	if ( this.goAnimation ) {
1625		this.goAnimation.stop();
1626	}
1627	
1628	
1629	var to = Object.assign( {}, location );	
1630	
1631	// limit lat
1632	to.lat = Math.min( this.maxLat(), to.lat );
1633	to.lat = Math.max( this.minLat(), to.lat );
1634	
1635
1636	var ani = {
1637		_end : function() {
1638			this.goAnimation = null;
1639			if ( this.zoomAnimation ) this.zoomAnimation.stop();
1640			this.zoomAnimation = null;
1641		},
1642		lerpLatLng : true
1643	};
1644	
1645	Object.assign( ani, options );
1646	
1647	this.goAnimation = this.animate( 'location', to, ani );
1648	
1649	
1650	if ( options.zoom ) {
1651		
1652		this.zoomAnimation = this.animate( 'zoom', options.zoom, {
1653			duration : this.goAnimation.duration
1654		} );
1655		
1656	}
1657	
1658	return this.goAnimation;
1659	
1660};
1661
1662
1663
1664// PUBLIC
1665// get position in element container for a lat/lng location with optional earth offset
1666
1667Earth.prototype.getPoint = function( location, offset ) {
1668	
1669	if ( ! offset ) offset = 0;
1670	
1671	var worldPos = Earth.latLngToWorld( location, Earth.earthRadius + offset );
1672	
1673	return Earth.worldToElement( worldPos, this.elementSize, this.camera );	
1674	
1675};
1676
1677
1678
1679// PUBLIC
1680// get lat/lng location for a mouse/touch position
1681
1682Earth.prototype.getLocation = function( point ) {
1683	
1684	var mouse = Earth.normalizeMouse( this, point.x, point.y );
1685	
1686	this.raycaster.setFromCamera( Earth.normalizeRaycast(mouse), this.camera );
1687	var intersects = this.raycaster.intersectObjects( [this.sphere] );
1688	
1689	for ( var i = 0; i < intersects.length; i++ ) {
1690		
1691		var uv = intersects[ i ].uv;
1692		intersects[ i ].object.material.map.transformUv( uv );
1693		
1694		return Earth.uvToLatLng( uv );
1695		
1696	}
1697	
1698	return false;
1699	
1700};
1701
1702
1703
1704// PUBLIC
1705// get approx. earth radius in pixels
1706
1707Earth.prototype.getRadius = function() {
1708	
1709	return this.elementSize.y / 2 * 0.758 *
1710		Math.pow( this.zoom, 
1711			(this.zoom > 1) ? 1.158 : 
1712			THREE.Math.lerp( 1.01, 1.11, this.zoom )
1713		);
1714	
1715};
1716
1717
1718
1719// static const
1720
1721Earth.earthRadius = 8;
1722Earth.camDistance = 24;
1723Earth.textureSize = [ 0, 512, 1024, 2048, 4096, 8192, 16384 ];
1724Earth.shadowSize  = [ 0, 512,  512, 1024, 2048, 4096,  8192 ];
1725Earth.anisotropy = [ 1, 1, 2, 4, 8, 8, 16 ];
1726
1727Earth.defaultImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj48Y2lyY2xlIGZpbGw9IiNmZmYiIGN4PSIxNiIgY3k9IjE2IiByPSIxNiIvPjwvc3ZnPg';
1728Earth.spriteHotspot = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iNjRweCIgaGVpZ2h0PSI2NHB4IiB2aWV3Qm94PSIwIDAgNjQgNjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDY0IDY0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZmlsbD0iIzAwRkYwMCIgZD0iTTEsMXY2Mmg2MlYxSDF6IE02Miw2MkgyVjJoNjBWNjJ6Ii8+PC9zdmc+';
1729
1730Earth.invisibleMaterial = new THREE.MeshBasicMaterial( {visible: false} );
1731Earth.hotspotMaterial = new THREE.MeshBasicMaterial( {color: 0x00ff00, wireframe: true} );
1732
1733
1734Earth.up = new THREE.Vector3( 0, 1, 0 );
1735Earth.left = new THREE.Vector3( 1, 0, 0 );
1736Earth.back = new THREE.Vector3( 0, 0, 1 );
1737Earth.zero = new THREE.Vector3( 0, 0, 0 );
1738
1739Earth.zeroMomentum = new THREE.Vector2();
1740
1741Earth.maxClickDistance = 10;
1742
1743
1744// static vars
1745
1746Earth.capabilitiesChecked = false;
1747Earth.cssAdded = false;
1748Earth.meshesAdded = false;
1749
1750Earth.meshes = {};
1751Earth.textures = {};
1752
1753
1754// static functions
1755
1756
1757// private
1758// fix the corrpuption of the svg namespace with some webservers
1759
1760Earth.fixSvgNamespace = function( svg ) {
1761	return String(svg).replace('xmlns="https://www.w3.org/2000/svg"', 'xmlns="ht'+'tp://www.w3.org/2000/svg"');
1762};
1763
1764
1765// private
1766// lat/lng values to numbers
1767
1768Earth.formatLatLng = function( latlng ) {
1769	if ( typeof latlng != "object" ) {
1770		return { lat: 0, lng: 0 };
1771	} else {
1772		latlng.lat = Number(latlng.lat);
1773		latlng.lng = Number(latlng.lng);
1774		return latlng;
1775	}	
1776};
1777
1778
1779// private
1780// get a simple hash of a string
1781
1782Earth.hash = function( str ){
1783    var hash = 0;
1784    if (str.length == 0) return hash;
1785    for (var i = 0; i < str.length; i++) {
1786        hash = ((hash<<5)-hash) + str.charCodeAt(i);
1787        hash = hash & hash;
1788    }
1789    return hash;
1790};
1791
1792
1793// private
1794// load a texture or get it from textures array if it exists
1795
1796Earth.getTexture = function ( src, resolution, alphaOnly ) {
1797	
1798	// build a unique key of source and options
1799	var key = Earth.hash(src) + '#' + resolution + ((alphaOnly)?'#a':'');
1800	
1801	// return existing texture
1802	if ( Earth.textures[key] ) return Earth.textures[key];
1803	
1804	
1805	var canvas = document.createElement("canvas");
1806	canvas.width = resolution;
1807	canvas.height = resolution;
1808
1809	Earth.textures[key] = new THREE.CanvasTexture( canvas );
1810	Earth.textures[key].needsUpdate = false;
1811	
1812	
1813	var img = new Image();
1814
1815	img.onload = function () {
1816
1817		// scale to contain
1818
1819		var ratio = img.naturalHeight / img.naturalWidth;
1820		var x = 0, y = 0, w = canvas.width, h = canvas.height;
1821		
1822		if ( ratio < 1 ) { // landscape
1823			h = Math.round( w * ratio );
1824			y = Math.round( canvas.height / 2 - h / 2 );
1825		} else { // portrait
1826			w = Math.round( h * (1/ratio) );
1827			x = Math.round( canvas.width / 2 - w / 2 );
1828		}
1829		
1830
1831		var ctx = canvas.getContext("2d");
1832
1833		// base color with 1% alpha
1834		ctx.globalAlpha = 1/256;
1835		ctx.fillStyle = "#999999";
1836		ctx.fillRect( 0, 0, canvas.width, canvas.height);
1837		ctx.globalAlpha = 1;
1838
1839		// draw original image
1840		ctx.globalCompositeOperation = "source-over";
1841		ctx.drawImage( img, x, y, w, h );
1842		
1843		
1844		// only change colors, not alpha
1845		ctx.globalCompositeOperation = "source-atop";
1846		
1847		if ( alphaOnly ) { // tint white
1848		
1849			ctx.fillStyle = "white";
1850			ctx.fillRect( 0, 0, canvas.width, canvas.height );
1851		
1852		} else { // bleed colors
1853			
1854			var offset = [-1,-1, 0,-1, 1,-1, -1,0, 1,0, -1,1, 0,1, 1,1];
1855			var s = 2;  // thickness scale
1856			  
1857			// draw 8 images at offsets from the array scaled by s
1858			for (var i=0; i < offset.length; i += 2) ctx.drawImage(img, x + offset[i]*s, y + offset[i+1]*s, w, h);
1859
1860			// image at original position
1861			ctx.drawImage( img, x, y, w, h );
1862			
1863		}		
1864		
1865		Earth.textures[key].needsUpdate = true;
1866	  
1867	};
1868
1869	img.src = src;
1870	
1871	
1872	return Earth.textures[key];
1873
1874};
1875
1876
1877// private
1878// limit resolution/quality to system capabilities
1879
1880Earth.checkCapabilities = function( renderer ) {
1881	
1882	var maxTex = renderer.capabilities.maxTextureSize;
1883	var maxAni = renderer.capabilities.getMaxAnisotropy();
1884	
1885	for ( var i=0; i < Earth.textureSize.length; i++ ) {
1886		if ( Earth.textureSize[i] > maxTex ) Earth.textureSize[i] = maxTex;
1887		if ( Earth.shadowSize[i] > maxTex ) Earth.shadowSize[i] = maxTex;
1888		
1889		if ( Earth.anisotropy[i] > maxAni ) Earth.anisotropy[i] = maxAni;
1890	}
1891
1892	Earth.capabilitiesChecked = true;
1893	
1894};
1895
1896
1897// private
1898// register and dispatch custom load event when the script is loaded
1899
1900Earth.dispatchLoadEvent = function() {
1901	
1902	if ( typeof window.CustomEvent === "function" ) {
1903		var loadedEvent = new CustomEvent( "earthjsload" );	
1904	} else {
1905		var loadedEvent = document.createEvent( "CustomEvent" );
1906		loadedEvent.initCustomEvent( "earthjsload", false, false, undefined );
1907	}
1908	
1909	window.dispatchEvent( loadedEvent );
1910	
1911};
1912
1913
1914// private
1915// add default css from string
1916
1917Earth.addCss = function() {
1918
1919	Earth.styleElement = document.createElement('style');
1920
1921	if ( Earth.styleElement.styleSheet ) {
1922		Earth.styleElement.styleSheet.cssText = Earth.css;
1923	} else {
1924		Earth.styleElement.appendChild( document.createTextNode( Earth.css ) );
1925	}
1926	
1927	document.getElementsByTagName("head")[0].appendChild( Earth.styleElement );
1928
1929};
1930
1931
1932// PUBLIC
1933// parse meshes from obj file string
1934
1935Earth.addMesh = function( objString ) {
1936	 
1937	var lib = new Earth.ObjParser().parse( objString );
1938	
1939	lib.traverse( function ( child ) {
1940		
1941		if ( ! child.name ) return;
1942		
1943		Earth.meshes[ child.name.split('_')[0] ] = child;
1944		child.material = new THREE.MeshPhongMaterial( { color: 0xFF0000, shininess: 0.3, flatShading: false } );
1945		
1946	} );
1947	
1948};
1949
1950
1951// private
1952// dispose objects
1953
1954Earth.dispose = function( obj ) {
1955	
1956	while (obj.children.length > 0) { 
1957		Earth.dispose(obj.children[0]);
1958		obj.remove(obj.children[0]);
1959	}
1960	
1961	if (obj.geometry) obj.geometry.dispose();
1962	if (obj.material) obj.material.dispose();
1963	
1964};
1965
1966
1967// PUBLIC
1968// destroy earth to free memory
1969
1970Earth.prototype.destroy = function() {
1971	
1972	this.paused = true;
1973	
1974	Earth.dispose( this.scene );
1975	
1976	this.renderer.forceContextLoss();
1977	this.renderer.dispose();
1978	this.renderer.context = null;
1979	this.renderer.domElement.remove();
1980	this.renderer.domElement = null;
1981	
1982	this.orbit.dispose();
1983	
1984	this.removeHitTester();
1985	
1986	this.overlays = null;
1987	this.occludables = null;
1988	this.animations = null;
1989	this.element.earth = null;
1990	this.element = null;
1991	this.canvas = null;
1992
1993};
1994
1995
1996// PUBLIC
1997// check for webgl support
1998
1999Earth.isSupported = function( supportIE11 ) {
2000	
2001	try {
2002		
2003		if ( ! supportIE11 && Earth.isIE11() ) return false;
2004		
2005		var canvas = document.createElement( 'canvas' );
2006		return window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) );
2007
2008	} catch ( e ) {
2009		
2010		return false;
2011
2012	}
2013	
2014};
2015
2016
2017// private
2018// check for IE11
2019
2020Earth.isIE11 = function( ) {
2021	return !! window.MSInputMethodContext && !! document.documentMode;	
2022};
2023
2024
2025// private
2026// get points of a 3d line
2027
2028Earth.getPathPoints = function( fromPos, toPos, subdevisions, offset, offsetFlow, offsetEasing ) {
2029	
2030	offset += Earth.earthRadius;	
2031	
2032	var points = [ fromPos, toPos ];
2033	
2034	for (var i=0; i < subdevisions; i++) {
2035		points = Earth.subdividePath( points, offset );
2036	}
2037	
2038	
2039	if ( offsetFlow ) {
2040		for (var i=0; i < points.length; i++) {
2041			points[i].multiplyScalar( 1 + Earth.Animation.Easing[ offsetEasing ]( i / (points.length-1) ) * offsetFlow/10 );
2042		}
2043	}
2044	
2045	return points;
2046	
2047};
2048
2049
2050// private
2051// subdivide a 3d line to smooth it
2052
2053Earth.subdividePath = function( points, offset ) {
2054
2055	if ( points.length < 2 ) return [];
2056
2057	var new_points = [];
2058	
2059	new_points.push( points[0] );
2060	
2061	// from point
2062	var fromPoint = points[0];
2063	
2064	for (var i=1; i < points.length; i++) {
2065		
2066		// to point
2067		toPoint = points[i];
2068		
2069		// mid point
2070		new_points.push(
2071			new THREE.Vector3().lerpVectors( fromPoint, toPoint, 0.5 ).normalize().multiplyScalar( offset )
2072		);
2073		
2074		new_points.push( toPoint );
2075		
2076		fromPoint = toPoint;
2077		
2078	}
2079	
2080	return new_points;
2081	
2082};
2083
2084
2085// private
2086// normalized mouse position relative to element center
2087
2088Earth.mouseCenterOffset = function( mouse, earth, radius ) {
2089	var relMouse = new THREE.Vector2( mouse.x - earth.bounds.left - earth.bounds.width/2 , mouse.y - earth.bounds.top - earth.bounds.height/2 );
2090	return Math.min( 1, relMouse.length() / radius );
2091};
2092
2093
2094// private
2095// raycasting helpers
2096
2097Earth.normalizeMouse = function( earth, x, y ) {
2098	return  new THREE.Vector2( ( x - earth.bounds.left ) / earth.bounds.width, ( y - earth.bounds.top ) / earth.bounds.height );
2099};
2100Earth.normalizeElement = function( v2, element ) {
2101	return new THREE.Vector2( v2.x / element.x, v2.y / element.y );
2102};
2103Earth.normalizeRaycast = function( v2 ) {
2104	return new THREE.Vector2( ( v2.x * 2 ) - 1, - ( v2.y * 2 ) + 1 );
2105};
2106
2107
2108// private
2109// position conversion
2110
2111Earth.latLngToWorld = function( location, radius ){
2112	
2113	var phi   = THREE.Math.degToRad( -location.lat + 90 );
2114	var theta = THREE.Math.degToRad( location.lng + 180 );
2115   
2116   return new THREE.Vector3().setFromSphericalCoords( radius, phi, theta );
2117   
2118};
2119Earth.worldToLatLng = function( pos ) {
2120	
2121	var s = new THREE.Spherical().setFromVector3 ( pos );
2122	   
2123	var loc = {
2124		lat: THREE.Math.radToDeg( -s.phi ) + 90,
2125		lng: THREE.Math.radToDeg( s.theta ) + 180
2126	};
2127	
2128	Earth.wrapLatLng( loc );
2129   
2130	return loc;
2131   
2132};
2133
2134
2135// private
2136// UV <-> LatLng
2137
2138Earth.uvToLatLng = function( uv ) {
2139	
2140	return {
2141		lat: (0.5 - uv.y) * 180,
2142		lng: (uv.x - 0.5) * 360
2143	};
2144	
2145};
2146Earth.latLngToUv = function( latlng ) {
2147	
2148	latlng = Earth.wrapLatLng(  Earth.formatLatLng( latlng ) );
2149	
2150	return new THREE.Vector2(
2151		1 - (latlng.lng / -360 + 0.5),
2152		0.5 + latlng.lat / -180
2153	);
2154	
2155};
2156
2157
2158// private
2159// position helpers
2160
2161Earth.wrapLatLng = function( latlng ) {
2162
2163	latlng.lat = Earth.wrap( latlng.lat, -90, 90 );
2164	latlng.lng = Earth.wrap( latlng.lng, -180, 180 );
2165	
2166	return latlng;
2167	
2168};
2169Earth.wrapLngForLerp = function( fromLng, toLng ) {
2170	
2171	if ( Math.abs( toLng - fromLng ) > 180 ) {
2172		toLng = ( toLng < 0 ) ? toLng + 360 : toLng - 360;
2173	}
2174	
2175	return toLng;
2176	
2177};
2178Earth.worldToElement = function( position, element, camera ) {
2179	
2180	var w = element.x / 2;
2181	var h = element.y / 2;
2182
2183	var pos = position.clone().project( camera );
2184	
2185	return new THREE.Vector2(
2186		( pos.x * w ) + w,
2187		- ( pos.y * h ) + h
2188	);
2189	
2190};
2191
2192
2193// private
2194// distance in world units for dashed lines
2195
2196Earth.getLineDistance = function( points ) {
2197	
2198	var distance = 0;
2199	var from = points[0];
2200	
2201	for ( var i = 1; i < points.length; i++ ) {
2202		var to = points[i];
2203		distance += from.distanceTo( to );
2204		from = to;
2205	} 
2206	
2207	return distance;
2208	
2209};
2210
2211
2212// private
2213// lerp angle for animation
2214
2215Earth.lerpAngle = function( fromA, toA, t ) {
2216    var fullA = Math.PI * 2;
2217    var a = (toA - fromA) % fullA;
2218    return fromA + ( 2 * a % fullA - a ) * t;
2219};
2220
2221
2222// private
2223// wrap number
2224
2225Earth.wrap = function( val, min, max ) {
2226	var range = max - min;
2227	return min + ( ( ( (val - min) % range ) + range ) % range );
2228};
2229
2230
2231// PUBLIC
2232// get distance in km
2233
2234Earth.getDistance = function( from, to ) {
2235	
2236	from =  Earth.formatLatLng( from );
2237	to =  Earth.formatLatLng( to );
2238	
2239	var degLat = THREE.Math.degToRad( to.lat - from.lat );
2240	var degLng = THREE.Math.degToRad( to.lng - from.lng );
2241	
2242	var a = Math.sin(degLat / 2) * Math.sin(degLat / 2) +
2243	Math.cos( THREE.Math.degToRad( from.lat ) ) * Math.cos( THREE.Math.degToRad( to.lat ) ) *
2244	Math.sin( degLng / 2 ) * Math.sin( degLng / 2 );
2245	
2246	var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
2247	
2248	return  6378.137 * c; // mean radius in km
2249	
2250};
2251
2252
2253// PUBLIC
2254// get angle between two latlng positions
2255
2256Earth.getAngle = function( from, to ) {
2257	
2258	from =  Earth.formatLatLng( from );
2259	to =  Earth.formatLatLng( to );
2260	
2261	var fromPos = Earth.latLngToWorld( from, Earth.earthRadius );
2262	var toPos = Earth.latLngToWorld( to, Earth.earthRadius );
2263	
2264	return THREE.Math.radToDeg( fromPos.angleTo( toPos ) );
2265	
2266};
2267
2268
2269// PUBLIC		lerp latlng location
2270// from			latlng
2271// to			latlng
2272// t			time 0-1
2273// lerpLatLng	lerp latlng values or 3d position
2274
2275Earth.lerp = function( from, to, t, lerpLatLng ) {
2276	
2277	from =  Earth.formatLatLng( from );
2278	to =  Earth.formatLatLng( to );
2279
2280	if ( lerpLatLng ) {
2281		
2282		return Earth.wrapLatLng( {
2283			lat: THREE.Math.lerp( from.lat, to.lat, t ),
2284			lng: THREE.Math.lerp( from.lng, Earth.wrapLngForLerp( from.lng, to.lng ), t )
2285		} );
2286		
2287	} else {
2288
2289		return Earth.worldToLatLng(
2290			Earth.latLngToWorld( from, Earth.earthRadius ).lerp( Earth.latLngToWorld( to, Earth.earthRadius ), t )
2291		);
2292	
2293	}
2294	
2295};
2296
2297
2298// private
2299// any event in types array registered?
2300
2301Earth.hasEvent = function( obj, types ) {
2302	
2303	if ( ! obj._listeners ) return false;
2304	
2305	for ( var i=0; i < types.length; i++ ) {
2306		if ( obj._listeners[ types[i] ] && obj._listeners[ types[i] ].length ) return true;
2307	}
2308	
2309	return false;
2310	
2311};
2312
2313
2314// private
2315// get mouse or touch client position
2316
2317Earth.getEventPosition = function( event ) {
2318	
2319	if ( event.type == 'touchstart' || event.type == 'touchmove' ) {
2320		return new THREE.Vector2( event.touches[0].clientX, event.touches[0].clientY );
2321	
2322	} else if ( event.type == 'touchend' ) {
2323		return new THREE.Vector2( event.changedTouches[0].clientX, event.changedTouches[0].clientY );
2324		
2325	} else {
2326		return new THREE.Vector2(  event.clientX, event.clientY );
2327		
2328	}
2329	
2330};
2331
2332
2333
2334/* MARKER */
2335
2336Earth.Marker = function( initOptions, thisEarth ) {
2337
2338	this.init( initOptions, thisEarth );
2339
2340};
2341
2342
2343// private
2344
2345Earth.Marker.prototype.init = function( initOptions, thisEarth ) {
2346	
2347	// default options
2348	
2349	var defaults = {
2350		
2351		isMarker : true,
2352		earth : thisEarth,
2353		
2354		location : { lat : 0, lng : 0 },
2355
2356		offset: 0,
2357		
2358		mesh: ["Pin", "Needle"],
2359		color: '#FF0000',
2360		color2: '#AAAAAA',
2361		color3: '#AAAAAA',
2362		color4: '#AAAAAA',
2363		color5: '#AAAAAA',
2364		color6: '#AAAAAA',
2365		color7: '#AAAAAA',
2366		color8: '#AAAAAA',
2367		
2368		scale : 1,
2369		scaleX : 1,
2370		scaleY : 1,
2371		scaleZ : 1,
2372		visible : true,
2373		
2374		hotspot : false,
2375		hotspotRadius : 0.5,
2376		hotspotHeight : 1.5,
2377
2378		align : true,
2379		rotationX : 0,
2380		rotationY : 0,
2381		rotationZ : 0,		
2382		lookAt : false,
2383		lookAngle: 0, // private
2384		
2385		shininess: 0.3,
2386		flatShading: false,
2387		castShadow: thisEarth.shadows,
2388		receiveShadow : false,
2389		transparent : ( typeof initOptions.opacity != 'undefined' ),
2390		
2391	};
2392	
2393	this.options = Object.assign(defaults, initOptions);
2394	
2395	
2396	this.createObject();
2397	
2398	this.addMeshes();
2399	
2400	
2401	// set props
2402	Object.assign( this, this.options );
2403
2404	this.ready = true;
2405	this.update();
2406	this.updateScale();
2407
2408	thisEarth.occludables.push( this );
2409	
2410	if ( this.options.transparent ) {
2411		this.addEventListener( 'occlusion', function() {
2412			for ( var i=0; i < this.object3d.children.length; i++ ) {
2413				this.object3d.children[i].renderOrder = ( this.occluded ) ? -4 : 0;
2414			}
2415		} );	
2416	}
2417	
2418	return this;
2419	
2420};
2421
2422
2423// private
2424// add container object and hotspot
2425
2426Earth.Marker.prototype.createObject = function() {
2427	
2428	if ( this.options.hotspot ) {
2429	
2430		if ( this.options.hotspotGeometry == 'sprite' ) { // sprite
2431			this.object3d = new THREE.Sprite( new THREE.SpriteMaterial( {
2432				map : (this.options.earth.showHotspots) ? Earth.getTexture( Earth.spriteHotspot, 64, false ) : null,
2433				opacity : (this.options.earth.showHotspots) ? 1 : 0,
2434			} ) );			
2435
2436		} else if ( this.options.hotspotGeometry == 'circle' ) { // circle
2437			var geometry = new THREE.CircleGeometry( this.options.hotspotRadius * 1.2, 8 );
2438			geometry.rotateX( THREE.Math.degToRad(-90) );
2439			geometry.translate( 0, 0.01, 0 );
2440
2441			this.object3d = new THREE.Mesh( geometry, (this.options.earth.showHotspots) ? Earth.hotspotMaterial : Earth.invisibleMaterial );
2442
2443		} else { // cylinder
2444			var geometry = new THREE.CylinderBufferGeometry( this.options.hotspotRadius, this.options.hotspotRadius*0.75, this.options.hotspotHeight, 5 );
2445			geometry.translate( 0, this.options.hotspotHeight/2, 0 );
2446			
2447			this.object3d = new THREE.Mesh( geometry, (this.options.earth.showHotspots) ? Earth.hotspotMaterial : Earth.invisibleMaterial );
2448			
2449		}
2450		
2451	
2452	} else {
2453
2454		this.object3d = new THREE.Object3D();
2455		
2456	}
2457	
2458	this.object3d.userData.marker = this;	
2459	this.options.earth.scene.add( this.object3d );
2460	
2461};
2462
2463
2464// private
2465// add geometry
2466
2467Earth.Marker.prototype.addMeshes = function() {
2468	
2469	if ( typeof this.options.mesh == "string" ) {
2470		var meshes = ( this.options.mesh ) ? [ this.options.mesh ] : [];
2471	} else {
2472		var meshes = this.options.mesh;
2473	}
2474	
2475	for ( var i = 0; i < meshes.length; i++ ) {
2476		
2477		var meshClone = Earth.meshes[ meshes[i] ].clone();
2478		meshClone.material = meshClone.material.clone();
2479		this.object3d.add( meshClone );
2480		
2481	}
2482	
2483};
2484
2485
2486Object.assign( Earth.Marker.prototype, Earth.ClassicEventDispatcher.prototype );
2487
2488
2489// PUBLIC
2490// properties
2491
2492Earth.sharedObjectProperties = {
2493	
2494	color: {
2495		get: function() {
2496			if ( ! this.object3d.children[0] ) return '#FFFFFF';
2497			return '#' + this.object3d.children[0].material.color.getHexString();
2498		},
2499		set: function( v ) {
2500			if ( ! this.object3d.children[0] ) return;
2501			this.object3d.children[0].material.color.set( v );
2502		}
2503	},
2504	
2505	opacity : {
2506		get: function() {
2507			if ( ! this.object3d.children[0] ) return 1;
2508			return this.object3d.children[0].material.opacity;
2509		},
2510		set: function( v ) {
2511			for ( var i=0; i < this.object3d.children.length; i++ ) {
2512				this.object3d.children[i].material.opacity = v;
2513			}
2514		}
2515	},
2516
2517	visible : {
2518		get: function() {
2519			return this.object3d.visible;
2520		},
2521		set: function( v ) {
2522			this.object3d.visible = v;
2523		}
2524	},
2525
2526	scale : {
2527		get: function() {
2528			return this.options.scale;
2529		},
2530		set: function( v ) {
2531			this.options.scale = v;
2532			if ( this.ready ) this.updateScale();
2533		}
2534	},
2535	scaleX : {
2536		get: function() {
2537			return this.options.scaleX;
2538		},
2539		set: function( v ) {
2540			this.options.scaleX = v;
2541			if ( this.ready ) this.updateScale();
2542		}
2543	},
2544	scaleY : {
2545		get: function() {
2546			return this.options.scaleY;
2547		},
2548		set: function( v ) {
2549			this.options.scaleY = v;
2550			if ( this.ready ) this.updateScale();
2551		}
2552	},
2553	scaleZ : {
2554		get: function() {
2555			return this.options.scaleZ;
2556		},
2557		set: function( v ) {
2558			this.options.scaleZ = v;
2559			if ( this.ready ) this.updateScale();
2560		}
2561	},
2562	
2563	align : {
2564		get: function() {
2565			return this.options.align;
2566		},
2567		set: function( v ) {
2568			this.options.align = v;
2569			if ( this.ready ) this.update();
2570		}
2571	},	
2572	rotationX : {
2573		get: function() {
2574			return this.options.rotationX;
2575		},
2576		set: function( v ) {
2577			this.options.rotationX = v;
2578			if ( this.ready ) this.update();
2579		}
2580	},
2581	rotationY : {
2582		get: function() {
2583			return this.options.rotationY;
2584		},
2585		set: function( v ) {
2586			this.options.rotationY = v;
2587			if ( this.ready ) this.update();
2588		}
2589	},
2590	rotationZ : {
2591		get: function() {
2592			return this.options.rotationZ;
2593		},
2594		set: function( v ) {
2595			this.options.rotationZ = v;
2596			if ( this.ready ) this.update();
2597		}
2598	},
2599	lookAt : {
2600		get: function() {
2601			return this.options.lookAt;
2602		},
2603		set: function( v ) {
2604			this.options.lookAt = (! v) ? false : Earth.formatLatLng(v);
2605			if ( this.ready ) this.update();
2606		}
2607	},
2608	lookAngle : {
2609		get: function() {
2610			return this.options.lookAngle;
2611		},
2612		set: function( v ) {
2613			this.options.lookAngle = v;
2614			if ( this.ready ) this.update( true );
2615		}
2616	},
2617	
2618	offset : {
2619		get: function() {
2620			return this.options.offset;
2621		},
2622		set: function( v ) {
2623			this.options.offset = v;
2624			if ( this.ready ) this.update();
2625		}
2626	},
2627	
2628	location : {
2629		get: function() {
2630			return this.options.location;
2631		},
2632		set: function( v ) {
2633			this.options.location = Earth.formatLatLng(v);
2634			if ( this.ready ) this.update();
2635		}
2636	}
2637	
2638};
2639
2640Object.defineProperties( Earth.Marker.prototype, Earth.sharedObjectProperties );
2641
2642Object.defineProperties( Earth.Marker.prototype, {
2643	
2644	color2: {
2645		get: function() {
2646			if ( ! this.object3d.children[1] ) return '#FFFFFF';
2647			return '#' + this.object3d.children[1].material.color.getHexString();
2648		},
2649		set: function( v ) {
2650			if ( ! this.object3d.children[1] ) return;
2651			this.object3d.children[1].material.color.set( v );
2652		}
2653	},
2654	color3: {
2655		get: function() {
2656			if ( ! this.object3d.children[2] ) return '#FFFFFF';
2657			return '#' + this.object3d.children[2].material.color.getHexString();
2658		},
2659		set: function( v ) {
2660			if ( ! this.object3d.children[2] ) return;
2661			this.object3d.children[2].material.color.set( v );
2662		}
2663	},
2664	color4: {
2665		get: function() {
2666			if ( ! this.object3d.children[3] ) return '#FFFFFF';
2667			return '#' + this.object3d.children[3].material.color.getHexString();
2668		},
2669		set: function( v ) {
2670			if ( ! this.object3d.children[3] ) return;
2671			this.object3d.children[3].material.color.set( v );
2672		}
2673	},
2674	color5: {
2675		get: function() {
2676			if ( ! this.object3d.children[4] ) return '#FFFFFF';
2677			return '#' + this.object3d.children[4].material.color.getHexString();
2678		},
2679		set: function( v ) {
2680			if ( ! this.object3d.children[4] ) return;
2681			this.object3d.children[4].material.color.set( v );
2682		}
2683	},
2684	color6: {
2685		get: function() {
2686			if ( ! this.object3d.children[5] ) return '#FFFFFF';
2687			return '#' + this.object3d.children[5].material.color.getHexString();
2688		},
2689		set: function( v ) {
2690			if ( ! this.object3d.children[5] ) return;
2691			this.object3d.children[5].material.color.set( v );
2692		}
2693	},
2694	color7: {
2695		get: function() {
2696			if ( ! this.object3d.children[6] ) return '#FFFFFF';
2697			return '#' + this.object3d.children[6].material.color.getHexString();
2698		},
2699		set: function( v ) {
2700			if ( ! this.object3d.children[6] ) return;
2701			this.object3d.children[6].material.color.set( v );
2702		}
2703	},
2704	color8: {
2705		get: function() {
2706			if ( ! this.object3d.children[7] ) return '#FFFFFF';
2707			return '#' + this.object3d.children[7].material.color.getHexString();
2708		},
2709		set: function( v ) {
2710			if ( ! this.object3d.children[7] ) return;
2711			this.object3d.children[7].material.color.set( v );
2712		}
2713	},
2714	
2715	shininess: {
2716		get: function() {
2717			if ( ! this.object3d.children[0] ) return 0.3;
2718			return this.object3d.children[0].material.shininess / 100;
2719		},
2720		set: function( v ) {
2721			for ( var i=0; i < this.object3d.children.length; i++ ) {
2722				this.object3d.children[i].material.shininess = v * 100;
2723			}
2724		}
2725	},	
2726	flatShading: {
2727		get: function() {
2728			if ( ! this.object3d.children[0] ) return 0.3;
2729			return this.object3d.children[0].material.flatShading;
2730		},
2731		set: function( v ) {
2732			for ( var i=0; i < this.object3d.children.length; i++ ) {
2733				this.object3d.children[i].material.flatShading = v;
2734			}
2735		}
2736	},
2737	castShadow : {
2738		get: function() {
2739			if ( ! this.object3d.children[0] ) return false;
2740			return this.object3d.children[0].castShadow;
2741		},
2742		set: function( v ) {
2743			for ( var i=0; i < this.object3d.children.length; i++ ) {
2744				this.object3d.children[i].castShadow = v;
2745			}
2746		}
2747	},
2748	receiveShadow: {
2749		get: function() {
2750			if ( ! this.object3d.children[0] ) return false;
2751			return this.object3d.children[0].receiveShadow;
2752		},
2753		set: function( v ) {
2754			for ( var i=0; i < this.object3d.children.length; i++ ) {
2755				this.object3d.children[i].receiveShadow = v;
2756			}
2757		}
2758	},
2759	
2760	transparent : {
2761		get: function() {
2762			if ( ! this.object3d.children[0] ) return false;
2763			return this.object3d.children[0].material.transparent;
2764		},
2765		set: function( v ) {
2766			for ( var i=0; i < this.object3d.children.length; i++ ) {
2767				this.object3d.children[i].material.transparent = v;
2768			}
2769		}
2770	},
2771
2772	
2773});
2774
2775
2776// PUBLIC
2777// remove object
2778
2779Earth.Marker.prototype.remove = function() {
2780
2781	this.stopAllAnimations();
2782	
2783	if ( this == this.earth.mouseOverObject ) {
2784		this.earth.mouseOverObject.dispatchEvent( { type: "mouseout" } );
2785		this.earth.mouseOverObject = null;
2786	}
2787	
2788	Earth.dispose( this.object3d );	
2789	this.earth.scene.remove( this.object3d );
2790
2791
2792	this.removed = true;
2793	
2794	var objIndex = this.earth.occludables.indexOf( this );
2795	if ( objIndex == -1 ) return; // already removed
2796	
2797	this.earth.occludables.splice( objIndex , 1 );
2798		
2799};
2800
2801
2802// private
2803// apply scaling
2804
2805Earth.Marker.prototype.updateScale = function() {
2806	
2807	if ( this.isSprite ) {
2808		
2809		this.object3d.scale.set(
2810			this.options.scaleX * this.options.scale * (this.options.hotspotRadius*2),
2811			this.options.scaleY * this.options.scale * (this.options.hotspotRadius*2),
2812			1
2813		);
2814		
2815		var reverse_scale = 1 / (this.options.hotspotRadius*2);
2816		
2817		this.object3d.children[0].scale.set(
2818			reverse_scale,
2819			reverse_scale,
2820			1
2821		);
2822		
2823		
2824	} else {
2825	
2826		this.object3d.scale.set(
2827			this.options.scaleX * this.options.scale,
2828			this.options.scaleY * this.options.scale,
2829			this.options.scaleZ * this.options.scale
2830		);
2831		
2832	}
2833	
2834};
2835
2836
2837// private
2838// get object quaternion
2839
2840Earth.Marker.prototype.getQuaternion = function() {
2841	
2842	if ( this.align ) { // rotate to world center
2843
2844		var qt = new THREE.Quaternion().setFromRotationMatrix(
2845			new THREE.Matrix4().lookAt( this.object3d.position, Earth.zero, Earth.up )
2846		);
2847		qt.multiply(  new THREE.Quaternion().setFromAxisAngle( Earth.left, 1.5707963267948966 ) );  // 90deg
2848		
2849	} else {
2850		
2851		var qt = new THREE.Quaternion();
2852		
2853	}
2854	
2855	return qt;
2856	
2857};
2858
2859
2860// private
2861// update object position and rotation
2862
2863Earth.Marker.prototype.update = function( fixLookAngle) {
2864	
2865	var p = Earth.latLngToWorld( this.location, Earth.earthRadius + this.offset );
2866	
2867	if ( this.isSprite ) {
2868		this.object3d.center = new THREE.Vector2( this.anchorX, this.anchorY );
2869		this.object3d.children[0].center = new THREE.Vector2( this.anchorX, this.anchorY );
2870	}
2871	
2872	
2873	this.object3d.position.copy( p );
2874	
2875	
2876	var qt = this.getQuaternion();
2877	
2878	if ( this.lookAt ) {
2879		if ( ! fixLookAngle ) {
2880			// update angle if lookAt is not same as location
2881			if ( Math.abs(this.location.lat-this.lookAt.lat) > 0.000001 || Math.abs(this.location.lng-this.lookAt.lng) > 0.000001 ) {
2882				this.options.lookAngle = this.getLocalAngle( p, qt, Earth.latLngToWorld( this.lookAt, Earth.earthRadius ) );
2883			}
2884		}
2885		qt.multiply(  new THREE.Quaternion().setFromAxisAngle( Earth.up, this.options.lookAngle ) );
2886	}
2887	
2888	if ( this.rotationX ) {
2889		qt.multiply(  new THREE.Quaternion().setFromAxisAngle( Earth.left, THREE.Math.degToRad(this.rotationX) ) );
2890	}
2891	if ( this.rotationY ) {
2892		qt.multiply(  new THREE.Quaternion().setFromAxisAngle( Earth.up, THREE.Math.degToRad(this.rotationY) ) );
2893	}
2894	if ( this.rotationZ ) {
2895		qt.multiply(  new THREE.Quaternion().setFromAxisAngle( Earth.back, THREE.Math.degToRad(this.rotationZ) ) );
2896	}
2897	
2898	this.object3d.setRotationFromQuaternion( qt );
2899	
2900};
2901
2902
2903// private
2904// get lookAt rotation
2905
2906Earth.Marker.prototype.getLocalAngle = function( p, qt, lookP ) {
2907
2908	var startAngle = 0;
2909	var angle = Math.PI / 2;
2910	var tests = 9;
2911	var testPoint = new THREE.Vector3( 0.01, 0, 0 );
2912	var distP, distM, prevDist;
2913	
2914	for ( var i = 0; i < tests; i++ ) {
2915		
2916		var rotP = p.clone().add( testPoint.clone().applyQuaternion(
2917			qt.clone().multiply(  new THREE.Quaternion().setFromAxisAngle( Earth.up, startAngle + angle ) )
2918		) );
2919		distP = lookP.distanceToSquared( rotP );
2920		
2921		var rotM = p.clone().add( testPoint.clone().applyQuaternion(
2922			qt.clone().multiply(  new THREE.Quaternion().setFromAxisAngle( Earth.up, startAngle - angle ) )
2923		) );
2924		distM = lookP.distanceToSquared( rotM );
2925		
2926		
2927		if ( ! prevDist || distP < prevDist || distM < prevDist ) {  // one tested point is closer
2928		
2929			if ( distP < distM ) {
2930				startAngle += angle;
2931				prevDist = distP;
2932			} else {
2933				startAngle -= angle;
2934				prevDist = distM;				
2935			}
2936			
2937		}
2938	
2939		angle /= 2;
2940	
2941	}
2942	
2943	return startAngle;
2944
2945};
2946
2947
2948
2949/* IMAGE */
2950/* >>> */
2951
2952Earth.Image = function( initOptions, thisEarth ) {
2953	
2954	this.init( initOptions, thisEarth );
2955	
2956};
2957	
2958Object.assign( Earth.Image.prototype, Earth.Marker.prototype );
2959Object.defineProperties( Earth.Image.prototype, Earth.sharedObjectProperties );
2960
2961Earth.Image.prototype.init = function( initOptions, thisEarth ) {
2962	
2963	// default options
2964	
2965	var defaults = {
2966		
2967		isImage : true,
2968		earth : thisEarth,
2969		
2970		location : { lat : 0, lng : 0 },
2971		offset: 0,
2972		
2973		image: Earth.defaultImage,
2974		imageResolution: 128,
2975		imageAlphaOnly: false,
2976		color: '#FFFFFF',
2977		
2978		scale : 1,
2979		scaleX : 1,
2980		scaleY : 1, // private
2981		scaleZ : 1,
2982		visible : true,
2983		
2984		hotspot : false,
2985		hotspotRadius : 0.5,
2986		hotspotGeometry : 'circle', // private
2987
2988		align : true,
2989		rotationX : 0,
2990		rotationY : 0,
2991		rotationZ : 0,		
2992		lookAt : false,
2993		lookAngle: 0, // private
2994		
2995		shininess: 0.3,
2996		flatShading: false,
2997		castShadow: false,
2998		receiveShadow : false,
2999		transparent : true,
3000		
3001		opacity: 1
3002		
3003	};
3004	
3005	this.options = Object.assign(defaults, initOptions);
3006	
3007	
3008	this.createObject();
3009	this.createImage();
3010	
3011	
3012	// set props
3013	Object.assign( this, this.options );
3014
3015	this.ready = true;
3016	this.update();
3017	this.updateScale();
3018
3019	thisEarth.occludables.push( this );
3020	
3021	if ( this.options.transparent ) {
3022		this.addEventListener( 'occlusion', function() {
3023			this.object3d.children[0].renderOrder = ( this.occluded ) ? -4 : 0;
3024		} );	
3025	}
3026	
3027	return this;
3028	
3029};
3030
3031
3032// private
3033// add image plane
3034
3035Earth.Image.prototype.createImage = function() {
3036	
3037	var geometry = new THREE.PlaneGeometry();
3038	geometry.rotateX( THREE.Math.degToRad(-90) );
3039	geometry.translate( 0, 0.01, 0 );
3040
3041	
3042	var material =  new THREE.MeshBasicMaterial( {
3043		alphaTest: 2/256,
3044		color: this.options.color,
3045		opacity : this.options.opacity,
3046		transparent: true,
3047		map : ( this.options.image ) ? Earth.getTexture( this.options.image, this.options.imageResolution, this.options.imageAlphaOnly ) : null,
3048		side: THREE.DoubleSide
3049	} );
3050	
3051	material.map.anisotropy = Earth.anisotropy[ this.options.earth.quality ];
3052	
3053	var sprite = new THREE.Mesh( geometry, material );
3054
3055	this.object3d.add( sprite );
3056	
3057};
3058
3059
3060/* <<< */
3061
3062
3063
3064/* SPRITE */
3065/* >>> */
3066
3067Earth.Sprite = function( initOptions, thisEarth ) {
3068	
3069	this.init( initOptions, thisEarth );
3070	
3071};
3072	
3073Object.assign( Earth.Sprite.prototype, Earth.Marker.prototype );
3074Object.defineProperties( Earth.Sprite.prototype, Earth.sharedObjectProperties );
3075
3076
3077Earth.Sprite.prototype.init = function( initOptions, thisEarth ) {
3078	
3079	// default options
3080	
3081	var defaults = {
3082		
3083		isSprite : true,
3084		earth : thisEarth,
3085		
3086		location : { lat : 0, lng : 0 },
3087		
3088		image : Earth.defaultImage,
3089		imageResolution: 128,
3090		imageAlphaOnly: false,
3091		
3092		offset: 0,
3093		anchorX : 0.5,
3094		anchorY : 0.5,
3095		
3096		color: '#FFFFFF',
3097		
3098		scale : 1,
3099		scaleX : 1,
3100		scaleY : 1,
3101		scaleZ : 1, // private
3102		visible : true,
3103		
3104		hotspot : false,
3105		hotspotRadius : 0.5,
3106		hotspotGeometry : 'sprite', // private
3107
3108		rotation : 0,
3109		rotationX : 0, // private
3110		rotationY : 0, // private
3111		rotationZ : 0, // private
3112		
3113		opacity: 1,
3114		occlude : true
3115		
3116	};
3117	
3118	this.options = Object.assign(defaults, initOptions);
3119	
3120	
3121	this.createObject();
3122	this.createSprite();
3123	
3124	
3125	// set props
3126	Object.assign( this, this.options );
3127
3128	this.ready = true;
3129	this.update();
3130	this.updateScale();
3131	
3132	thisEarth.occludables.push( this );
3133	
3134	this.addEventListener( 'occlusion', function() {
3135		var back = this.occlude && this.occluded;
3136		
3137		// hotspot
3138		if ( this.object3d.material ) {
3139			this.object3d.material.depthTest = back;
3140			this.object3d.material.depthWrite = ! back;
3141			this.object3d.renderOrder = ( back ) ? -5 : 5;
3142		}
3143		// image
3144		this.object3d.children[0].material.depthTest = back;
3145		this.object3d.children[0].material.depthWrite = ! back;
3146		this.object3d.children[0].renderOrder = ( back ) ? -5 : 5;
3147	} );	
3148	
3149	return this;
3150	
3151};
3152
3153
3154// private
3155// add sprite
3156
3157Earth.Sprite.prototype.createSprite = function() {
3158	
3159	var material =  new THREE.SpriteMaterial( {
3160		alphaTest: 2/256,
3161		color: this.options.color,
3162		opacity : this.options.opacity,
3163		map : ( this.options.image ) ? Earth.getTexture( this.options.image, this.options.imageResolution, this.options.imageAlphaOnly ) : null
3164	} );
3165	
3166	var sprite = new THREE.Sprite( material );
3167
3168	this.object3d.add( sprite );
3169	
3170};
3171
3172
3173// PUBLIC
3174// properties
3175
3176Object.defineProperties( Earth.Sprite.prototype, {
3177	
3178	rotation: {
3179		get: function() {
3180			return THREE.Math.radToDeg( this.object3d.material.rotation );
3181		},
3182		set: function( v ) {
3183			if ( this.object3d.material ) this.object3d.material.rotation = THREE.Math.degToRad( v );
3184			this.object3d.children[0].material.rotation = THREE.Math.degToRad( v );
3185		}
3186	}
3187
3188} );
3189
3190
3191/* <<< */
3192
3193
3194
3195
3196
3197
3198/* POINTS */
3199/* >>> */
3200
3201Earth.Points = function( initOptions, thisEarth ) {
3202	
3203	this.init( initOptions, thisEarth );
3204	
3205};
3206	
3207Object.assign( Earth.Points.prototype, Earth.Marker.prototype );
3208
3209
3210Earth.Points.prototype.init = function( initOptions, thisEarth ) {
3211	
3212	// default options
3213	
3214	var defaults = {
3215		
3216		isPoints : true,
3217		earth : thisEarth,
3218		
3219		points : [],
3220		
3221		image : Earth.defaultImage,
3222		imageResolution: 128,
3223		imageAlphaOnly: false,
3224		
3225		color: '#FFFFFF',
3226		scale : 1,
3227		opacity : 1,
3228		clip: 1,		
3229		visible : true,
3230		
3231	};
3232	
3233	this.options = Object.assign(defaults, initOptions);
3234	
3235	
3236	this.createObject();
3237	
3238	
3239	// set props
3240	Object.assign( this, this.options );
3241	
3242	
3243	this.createPoints();
3244	
3245	
3246	var thisPoints = this;
3247	
3248	this.sortOnce = function() {
3249		thisPoints.sort();
3250		thisEarth.removeEventListener( 'update', this.sortOnce );
3251	};
3252
3253	this.sort = function() {
3254
3255		var vector = new THREE.Vector3();
3256
3257		// model view projection matrix
3258
3259		var matrix = new THREE.Matrix4();
3260		matrix.multiplyMatrices( thisEarth.camera.projectionMatrix, thisEarth.camera.matrixWorldInverse );
3261		matrix.multiply( thisPoints.object3d.matrixWorld );
3262
3263		var geometry = thisPoints.object3d.children[0].geometry;
3264
3265		var index = geometry.getIndex();
3266		var positions = geometry.getAttribute( 'position' ).array;
3267		var length = positions.length / 3;
3268
3269		if ( index === null ) {
3270
3271			var array = new Uint16Array( length );
3272
3273			for ( var i = 0; i < length; i ++ ) {
3274				array[ i ] = i;
3275			}
3276
3277			index = new THREE.BufferAttribute( array, 1 );
3278			geometry.setIndex( index );
3279
3280		}
3281
3282		var sortArray = [];
3283
3284		for ( var i = 0; i < length; i ++ ) {
3285			vector.fromArray( positions, i * 3 );
3286			vector.applyMatrix4( matrix );
3287
3288			sortArray.push( [ vector.z, i ] );
3289		}
3290
3291		function numericalSort( a, b ) {
3292			return b[ 0 ] - a[ 0 ];
3293		}
3294
3295		sortArray.sort( numericalSort );
3296
3297		var indices = index.array;
3298
3299		for ( var i = 0; i < length; i ++ ) {
3300			indices[ i ] = sortArray[ i ][ 1 ];
3301		}
3302
3303		geometry.index.needsUpdate = true;
3304
3305	};
3306	
3307	this.removeEvents = function() {
3308		thisEarth.removeEventListener( 'update', this.sortOnce );
3309		thisEarth.removeEventListener( 'change', this.sort );
3310	};
3311	
3312	
3313	thisEarth.addEventListener( 'update', this.sortOnce );
3314	thisEarth.addEventListener( 'change', this.sort );
3315	
3316	
3317	this.ready = true;
3318	
3319	return this;
3320	
3321};
3322
3323
3324
3325// private
3326// custom points shader
3327
3328Earth.Points.prototype.createPointsMaterial = function() {
3329	
3330	var vertexShaderSource = [
3331		'attribute float size;',
3332		'attribute vec3 customColor;',
3333		'attribute float opacity;',
3334		'varying float vAlpha;',
3335		'varying vec3 vColor;',
3336		'uniform float usize;',
3337		'void main() {',
3338		'	vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );',
3339		'	vColor = customColor;',
3340		//'	vAlpha = opacity * ( 5.0 / ( mvPosition.z * -1.0 - 15.0 ) );',
3341		'	vAlpha = opacity;',
3342		'	gl_PointSize = usize * size * ( 200.0 / -mvPosition.z );',
3343		'	gl_Position = projectionMatrix * mvPosition;',
3344		'}'
3345	];
3346	
3347	var fragmentShaderSource = [
3348		'uniform vec3 ucolor;',
3349		'uniform float ualpha;',
3350		'uniform sampler2D pointTexture;',
3351		'varying float vAlpha;',
3352		'varying vec3 vColor;',
3353		'void main() {',
3354		'	gl_FragColor = vec4( ucolor * vColor, ualpha * vAlpha ) * texture2D( pointTexture, vec2(gl_PointCoord.x, 1.0 - gl_PointCoord.y) );',
3355		'}'
3356	];
3357	
3358	return new THREE.ShaderMaterial({
3359		uniforms: {
3360			ucolor: {value: new THREE.Color( this.options.color )},
3361			ualpha: {value: this.options.opacity },
3362			usize: {value: this.options.scale },
3363			pointTexture: {value: ( this.options.image ) ? Earth.getTexture( this.options.image, this.options.imageResolution, this.options.imageAlphaOnly ) : null }
3364		},
3365		vertexShader: vertexShaderSource.join( '\r\n' ),
3366		fragmentShader: fragmentShaderSource.join( '\r\n' ),
3367		transparent: true,
3368		depthWrite : false
3369	});
3370	
3371};
3372
3373
3374// private
3375// add points
3376
3377Earth.Points.prototype.createPoints = function() {
3378	
3379	var geometry = new THREE.BufferGeometry();
3380	
3381	var data_pos = [];
3382	var data_scale = [];
3383	var data_color = [];
3384	var data_opacity = [];
3385
3386	for ( var i = 0; i < this.points.length; i++ ) {
3387		
3388		if ( ! this.points[i].location || typeof this.points[i].location.lat == 'undefined' ) this.points[i].location = { lat: 0, lng: 0 };
3389		
3390		var p = Earth.latLngToWorld( this.points[i].location, Earth.earthRadius + ((typeof this.points[i].offset != 'undefined') ? this.points[i].offset : 0.1) );
3391		data_pos.push( p.x, p.y, p.z );
3392		
3393		data_scale.push( (typeof this.points[i].scale != 'undefined') ? this.points[i].scale : 1 );
3394		
3395		var c =  new THREE.Color( this.points[i].color || 0xFFFFFF );
3396		data_color.push( c.r, c.g, c.b );
3397		
3398		data_opacity.push( (typeof this.points[i].opacity != 'undefined') ? this.points[i].opacity : 1 );
3399
3400	}
3401
3402	geometry.setAttribute( "position", new THREE.Float32BufferAttribute( data_pos, 3 ) );
3403	geometry.setAttribute( "customColor", new THREE.Float32BufferAttribute( data_color, 3 ) );
3404	geometry.setAttribute( "size", new THREE.Float32BufferAttribute( data_scale, 1 ) );
3405	geometry.setAttribute( "opacity", new THREE.Float32BufferAttribute( data_opacity, 1 ) );
3406
3407	var material = this.createPointsMaterial();
3408
3409	this.object3d.add( new THREE.Points( geometry, material ) );
3410	
3411};
3412
3413
3414// PUBLIC
3415// properties
3416
3417Object.defineProperties( Earth.Points.prototype, {
3418	
3419	visible : {
3420		get: function() {
3421			return this.object3d.visible;
3422		},
3423		set: function( v ) {
3424			this.object3d.visible = v;
3425		}
3426	},
3427	
3428	scale : {
3429		get: function() {
3430			return this.options.scale;
3431		},
3432		set: function( v ) {
3433			this.options.scale = v;
3434			if ( this.ready ) this.object3d.children[0].material.uniforms.usize.value = v;
3435		}
3436	},
3437	
3438	opacity : {
3439		get: function() {
3440			return this.options.opacity;
3441		},
3442		set: function( v ) {
3443			this.options.opacity = v;
3444			if ( this.ready ) this.object3d.children[0].material.uniforms.ualpha.value = v;
3445		}
3446	},
3447	
3448	color : {
3449		get: function() {
3450			return this.options.color;
3451		},
3452		set: function( v ) {
3453			this.options.color = v;
3454			if ( this.ready ) this.object3d.children[0].material.uniforms.ucolor.value = new THREE.Color( v );
3455		}
3456	}
3457
3458} );
3459
3460
3461// PUBLIC
3462// remove points
3463
3464Earth.Points.prototype.remove = function() {
3465	
3466	this.stopAllAnimations();
3467
3468	this.removeEvents();
3469	
3470	Earth.dispose( this.object3d );
3471	this.earth.scene.remove( this.object3d );
3472	this.removed = true;
3473	
3474};
3475
3476
3477/* <<< */
3478
3479
3480
3481
3482
3483
3484/* LINE */
3485/* >>> */
3486
3487Earth.Line = function( initOptions, thisEarth ) {
3488	
3489	// default options
3490	
3491	var defaults = {
3492		
3493		isLine : true,
3494		earth : thisEarth,
3495		
3496		locations : [],
3497		
3498		offset : 0.01,
3499		offsetFlow : 0,
3500		offsetEasing : 'arc',
3501
3502		hairline: false,
3503		width: 1,
3504		endWidth : -1,
3505		clip: 1,
3506		
3507		dashed : false,
3508		dashSize : 0.5,
3509		dashRatio : 0.5,
3510		dashOffset: 0,
3511		
3512		color: '#FF0000',
3513		opacity: 1,
3514		visible : true,
3515		alwaysBehind : false,
3516		alwaysOnTop : false,
3517		transparent: ( ( ! initOptions.hairline && initOptions.dashed ) || typeof initOptions.opacity != 'undefined' ) ,
3518		
3519	};
3520	
3521	this.options = Object.assign(defaults, initOptions);
3522	this.lineLength = 0;
3523	
3524	// create object
3525
3526	if ( this.options.hairline ) { // native gl line	
3527	
3528		this.object3d = new THREE.Line();
3529		this.object3d.material  = new THREE[ ( this.options.dashed ) ? 'LineDashedMaterial' : 'LineBasicMaterial' ]();
3530	
3531	} else {
3532		
3533		this.object3d = new THREE.Mesh();
3534		this.meshLine = new Earth.MeshLine();
3535		this.object3d.geometry = this.meshLine.geometry;
3536		this.lineGeometry = new THREE.BufferGeometry();
3537		this.object3d.material = new Earth.MeshLineMat();
3538	
3539	}
3540	
3541
3542	thisEarth.scene.add( this.object3d );
3543	
3544	
3545	// set props
3546	
3547	Object.assign( this, this.options );
3548	
3549	
3550	this.ready = true;
3551	this.updatePoints();
3552
3553	return this;
3554	
3555};
3556
3557
3558// PUBLIC
3559// properties
3560
3561Object.defineProperties( Earth.Line.prototype, {
3562	
3563	width : {
3564		get: function() {
3565			return this.options.width;
3566		},
3567		set: function( v ) {
3568			this.options.width = v;
3569			if ( ! this.options.hairline ) {
3570				this.object3d.material.uniforms.lineWidth.value = v / 10;
3571				if ( this.ready ) this.updateGeometry();
3572			}
3573		}
3574	},
3575	endWidth : {
3576		get: function() {
3577			return ( this.options.endWidth == -1 ) ? this.options.width : this.options.endWidth;
3578		},
3579		set: function( v ) {
3580			this.options.endWidth = v;
3581			if ( ! this.options.hairline ) {
3582				if ( this.ready ) this.updateGeometry();
3583			}
3584		}
3585	},
3586	
3587
3588	dashSize : {
3589		get: function() {
3590			return this.options.dashSize;
3591		},
3592		set: function( v ) {
3593			this.options.dashSize = v;
3594			if ( this.options.hairline ) {
3595				this.object3d.material.dashSize = v;
3596			} else {
3597				if ( this.ready ) this.updateDash();
3598			}
3599		}
3600	},
3601	dashRatio : {
3602		get: function() {
3603			return this.options.dashRatio;
3604		},
3605		set: function( v ) {
3606			this.options.dashRatio = Math.min(1, Math.max(0, v));
3607			if ( this.options.hairline ) {
3608				this.object3d.material.gapSize = this.options.dashSize * v * 2;
3609			} else {
3610				this.object3d.material.uniforms.dashRatio.value = v;
3611			}
3612		}
3613	},	
3614	dashOffset : {
3615		get: function() {
3616			return this.options.dashOffset;
3617		},
3618		set: function( v ) {
3619			this.options.dashOffset = v;
3620			if ( ! this.options.hairline ) {
3621				this.object3d.material.uniforms.dashOffset.value = v;
3622			}
3623		}
3624	},
3625	
3626	color : {
3627		get: function() {
3628			return this.options.color;
3629		},
3630		set: function( v ) {
3631			this.options.color = v;
3632			if ( this.options.hairline ) {
3633				this.object3d.material.color = new THREE.Color( v );
3634			} else {
3635				this.object3d.material.uniforms.color.value = new THREE.Color( v );
3636			}
3637		}
3638	},	
3639	opacity : {
3640		get: function() {
3641			return this.options.opacity;
3642		},
3643		set: function( v ) {
3644			this.options.opacity = v;
3645			if ( this.options.hairline ) {
3646				this.object3d.material.opacity = v;
3647			} else {
3648				this.object3d.material.uniforms.opacity.value = v;
3649			}
3650		}
3651	},
3652	visible : {
3653		get: function() {
3654			return this.object3d.visible;
3655		},
3656		set: function( v ) {
3657			this.object3d.visible = v;
3658		}
3659	},
3660
3661	alwaysBehind: {
3662		get: function() {
3663			return ! this.object3d.material.depthWrite;
3664		},
3665		set: function( v ) {
3666			this.object3d.material.depthWrite = ! v;
3667			this.object3d.renderOrder = ( v ) ? -1 : 0;
3668		}
3669	},
3670	
3671	transparent : {
3672		get: function() {
3673			return this.object3d.material.transparent;
3674		},
3675		set: function( v ) {
3676			this.object3d.material.transparent = v;
3677		}
3678	},
3679	
3680	clip : {
3681		get: function() {
3682			return this.options.clip;
3683		},
3684		set: function( v ) {
3685			this.options.clip = Math.min(1, Math.max(0, v));
3686			if ( this.ready ) this.updateClip();
3687		}
3688	},
3689	
3690	offset : {
3691		get: function() {
3692			return this.options.offset;
3693		},
3694		set: function( v ) {
3695			this.options.offset = v;
3696			if ( this.ready ) this.updatePoints();
3697		}
3698	},
3699	offsetFlow : {
3700		get: function() {
3701			return this.options.offsetFlow;
3702		},
3703		set: function( v ) {
3704			this.options.offsetFlow = v;
3705			if ( this.ready ) this.updatePoints();
3706		}
3707	},
3708	
3709	locations : {
3710		get: function() {
3711			return this.options.locations;
3712		},
3713		set: function( v ) {
3714			for ( var i=0; i < v.length; i++ ) {
3715				v[i] = Earth.formatLatLng( v[i] );
3716			}
3717			this.options.locations = v;
3718			if ( this.ready ) this.updatePoints();
3719		}
3720	},
3721	
3722});
3723
3724
3725// private
3726// update line segment positions
3727
3728Earth.Line.prototype.updatePoints = function() {
3729	
3730	this.points = [];
3731	
3732	if (  this.locations.length ) {
3733		var fromPos = Earth.latLngToWorld( this.locations[0], Earth.earthRadius + this.offset );
3734		
3735		for ( var i = 1; i < this.locations.length; i++ ) {
3736		
3737			var toPos = Earth.latLngToWorld( this.locations[i], Earth.earthRadius + this.offset );
3738
3739			var subdevisions = Math.ceil( Math.sqrt( ( 1 + fromPos.distanceTo(toPos) ) * this.earth.quality ) );
3740
3741			this.points.pop();
3742			this.points = this.points.concat( Earth.getPathPoints( fromPos, toPos, subdevisions, this.offset, this.offsetFlow, this.offsetEasing ) );
3743			
3744			fromPos = toPos;
3745			
3746		}
3747	}
3748	
3749	this.updateGeometry();
3750	
3751};
3752
3753
3754// private
3755// update line geometry
3756
3757Earth.Line.prototype.updateGeometry = function() {
3758	
3759	if ( this.hairline ) {
3760		
3761		this.object3d.geometry.setFromPoints( this.points );
3762	
3763	} else {
3764		
3765		this.lineGeometry.vertices = this.points;
3766		
3767		var thisLine = this;
3768		this.meshLine.setGeometry( this.lineGeometry, function( p ) {
3769			if ( thisLine.endWidth == -1 ) return 1;
3770			p *= thisLine.clip; // to complete path
3771			return THREE.Math.lerp( thisLine.width, thisLine.endWidth, p ) / thisLine.width;
3772		} );
3773		
3774		if ( this.dashed ) this.lineLength = Earth.getLineDistance( this.points );
3775		
3776	}
3777	
3778	this.updateClip();
3779	
3780	this.updateDash();
3781
3782};
3783
3784
3785// private
3786// handle clip property
3787
3788Earth.Line.prototype.updateClip = function() {
3789	
3790	if ( this.clip < 1 ) {
3791		this.object3d.geometry.setDrawRange( 0, Math.round( this.points.length * this.clip ) * ( this.hairline ? 1 : 6 ) );
3792	} else {
3793		this.object3d.geometry.setDrawRange( 0, Infinity );
3794	}
3795	
3796};
3797
3798
3799// private
3800// handle dashed property
3801
3802Earth.Line.prototype.updateDash = function( options ) {
3803	
3804	if ( ! this.dashed ) return;
3805	
3806	if ( this.hairline ) {
3807		this.object3d.computeLineDistances();
3808	} else {
3809		this.object3d.material.uniforms.useDash.value = 1;
3810		this.object3d.material.uniforms.dashArray.value = 1 / this.lineLength * this.dashSize * 3;
3811	}
3812	
3813};
3814
3815
3816// private
3817// remove line
3818
3819Earth.Line.prototype.remove = function() {
3820	
3821	this.stopAllAnimations();
3822	
3823	Earth.dispose( this.object3d );
3824	this.earth.scene.remove( this.object3d );
3825	this.removed = true;
3826	
3827};
3828
3829/* <<< */
3830
3831
3832
3833/* OVERLAY */
3834
3835Earth.Overlay = function( initOptions, thisEarth ) {
3836	
3837	// default options
3838	
3839	var defaults = {
3840		
3841		isOverlay: true,
3842		earth: thisEarth,
3843		
3844		location : { lat : 0, lng : 0 },
3845		offset : 0,
3846		
3847		content : '',
3848		className : '',
3849		
3850		visible : true,
3851		occlude : true,
3852		
3853		containerScale : 0,
3854		zoomScale : 1,
3855		depthScale : 0
3856		
3857	};
3858	
3859	this.options = Object.assign(defaults, initOptions);
3860	
3861	this.ready = false;
3862	
3863	
3864	// create overlay
3865	
3866	var div = document.createElement( 'div' );	
3867	thisEarth.element.appendChild( div );
3868	div.overlay = this;
3869	this.element = div;
3870	this.element.className = 'earth-overlay';
3871	
3872	// inner wrap
3873	this.element.appendChild( document.createElement( 'div' ) );
3874	
3875	
3876	thisEarth.overlays.push( this );
3877	
3878	
3879	// set props
3880	Object.assign(this, this.options);
3881
3882	return this;
3883	
3884};
3885
3886Object.assign( Earth.Overlay.prototype, Earth.ClassicEventDispatcher.prototype );
3887
3888
3889// PUBLIC
3890// properties
3891
3892Object.defineProperties( Earth.Overlay.prototype, {
3893	
3894	location : {
3895		get: function() {
3896			return this.options.location;
3897		},
3898		set: function( v ) {
3899			this.options.location = Earth.formatLatLng(v);
3900		}
3901	},
3902	
3903	content : {
3904		get: function() {
3905			return this.element.firstChild.innerHTML;
3906		},
3907		set: function( v ) {
3908			this.element.firstChild.innerHTML = v;
3909		}
3910	},
3911	
3912	className : {
3913		get: function() {
3914			return this.element.firstChild.className;
3915		},
3916		set: function( v ) {
3917			this.element.firstChild.className = v;
3918		}
3919	},
3920	
3921	visible : {
3922		get: function() {
3923			return this.options.visible;
3924		},
3925		set: function( v ) {
3926			this.options.visible = v;
3927			this.element.style.display = ( v && this.ready ) ? 'block' : 'none';
3928		}
3929	},
3930	
3931	occlude : {
3932		get: function() {
3933			return this.options.occlude;
3934		},
3935		set: function( v ) {
3936			this.options.occlude = v;
3937		}
3938	}
3939	
3940});
3941
3942
3943// PUBLIC
3944// remove overlay
3945
3946Earth.Overlay.prototype.remove = function() {
3947	
3948	this.stopAllAnimations();
3949	
3950	this.earth.element.removeChild( this.element );
3951	this.removed = true;
3952
3953	var objIndex = this.earth.overlays.indexOf( this );
3954	if ( objIndex == -1 ) return; // already removed
3955	this.earth.overlays.splice( objIndex , 1 );
3956	
3957};
3958
3959
3960
3961/* OCCLUDABLE */
3962// private
3963
3964Earth.Occludable = function() {};
3965
3966Earth.Occludable.prototype.updatePositions = function() {
3967	if ( this.isOverlay ) {
3968		var world_pos = Earth.latLngToWorld( this.location, Earth.earthRadius + this.offset );
3969	} else {
3970		var world_pos = this.object3d.position;
3971	}
3972	this.elementPosition = Earth.worldToElement( world_pos, this.earth.elementSize, this.earth.camera );
3973	this.distance = this.earth.camera.position.distanceTo( world_pos );			
3974	
3975};
3976
3977Earth.Occludable.prototype.updateOcclusion = function() {
3978	
3979	if ( this.distance < this.earth.camera.position.length() ) { // between earth and camera	
3980		var occluded = false;
3981		
3982	} else {
3983		var occluded = this.earth.elementCenter.distanceTo( this.elementPosition ) < this.earth.radius;
3984		
3985	}
3986	
3987	// check for change and dispatch event
3988	if ( typeof this.occluded == 'undefined' || this.occluded != occluded ) {
3989		this.occluded = occluded;
3990		this.dispatchEvent( { type: 'occlusion' } );
3991	}
3992	
3993};
3994
3995Object.assign( Earth.Marker.prototype, Earth.Occludable.prototype );
3996Object.assign( Earth.Image.prototype, Earth.Occludable.prototype );
3997Object.assign( Earth.Sprite.prototype, Earth.Occludable.prototype );
3998Object.assign( Earth.Overlay.prototype, Earth.Occludable.prototype );
3999
4000
4001
4002/* ANIMATION */
4003
4004Earth.Animation = function( options ) {
4005	
4006	Object.assign(this, options);	
4007	this.earth.animations.push( this );
4008	return this;
4009
4010};
4011
4012/* >>> */
4013
4014
4015// PUBLIC
4016// remove animation
4017
4018Earth.Animation.prototype.stop = function( dispatchComplete, jumpToEnd ) {
4019	
4020	if ( jumpToEnd ) {
4021		this.time = this.duration;
4022		this.step( 1 );
4023	}
4024	
4025	this.dispatchEvents( dispatchComplete );
4026	
4027	var aniIndex = this.earth.animations.indexOf( this );
4028	if ( aniIndex == -1 ) return; // not found
4029	
4030	this.earth.animations.splice( aniIndex , 1 );
4031	
4032};
4033
4034
4035// private
4036// call end / complete handlers
4037
4038Earth.Animation.prototype.dispatchEvents = function( dispatchComplete ) {
4039	
4040	if ( this._end ) { // internal use
4041		this._end.bind( this.target )( this );
4042	}
4043	
4044	if ( typeof this.end == 'function' ) {
4045		this.end.bind( this.target )( this );
4046	}
4047	
4048	if ( typeof this.complete == 'function' && dispatchComplete && ! this.target.removed ) {
4049		this.complete.bind( this.target )( this );
4050	}
4051	
4052};
4053
4054/* <<< */
4055
4056Earth.Animation.Easing = {
4057	
4058	'linear' : 		function (t) { return t },
4059	
4060	'in-quad' : 	function (t) { return t*t },
4061	'out-quad' : 	function (t) { return t*(2-t); },
4062	'in-out-quad' : function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t },
4063	
4064	'in-cubic' : 	function (t) { return t*t*t },
4065	'out-cubic' : 	function (t) { return (--t)*t*t+1 },
4066	'in-out-cubic' :function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 },
4067	
4068	'in-quart' :	function (t) { return t*t*t*t },
4069	'out-quart' :	function (t) { return 1-(--t)*t*t*t },
4070	'in-out-quart': function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t },
4071	
4072	'in-back' :		function (t) { return t*t*(2.70158*t - 1.70158); },
4073	'out-back' :	function (t) { return (t=t-1)*t*(2.70158*t + 1.70158) + 1; },
4074	'in-out-back' :	function (t) {
4075		var s = 1.70158;
4076		if ((t/=0.5) < 1) return 0.5*(t*t*(((s*=(1.525))+1)*t - s));
4077		return 0.5*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2);
4078	},
4079	
4080	'elastic' : function (t) {
4081		var p=.3;
4082		if (t==0) return 0; if (t==1) return 1;
4083		var s = p/(2*Math.PI) * Math.asin (1);
4084		return Math.pow(2,-10*t) * Math.sin( (t-s)*(2*Math.PI)/p ) + 1;
4085	},
4086	
4087	'bounce' : function (t) {
4088		if (t < (1/2.75)) {
4089			return (7.5625*t*t);
4090		} else if (t < (2/2.75)) {
4091			return (7.5625*(t-=(1.5/2.75))*t + .75);
4092		} else if (t < (2.5/2.75)) {
4093			return (7.5625*(t-=(2.25/2.75))*t + .9375);
4094		} else {
4095			return (7.5625*(t-=(2.625/2.75))*t + .984375);
4096		}
4097	},
4098	
4099	'arc' : function(t) {
4100		return t<.5 ? this['out-quad'](t*2) : this['out-quad'](2 - t*2);
4101	}
4102	
4103};
4104
4105
4106
4107/* ANIMATABLE */
4108
4109/* >>> */
4110
4111Earth.Animatable = function() {};
4112
4113Earth.Animatable.colorProp  = [ 'color', 'color2', 'color3', 'color4', 'color5', 'color6', 'color7', 'color8', 'lightColor', 'lightGroundColor' ];
4114Earth.Animatable.latlngProp = [ 'location', 'sunLocation' ];
4115Earth.Animatable.angleProp  = [ 'lookAt' ];
4116
4117
4118// PUBLIC
4119// start object animation
4120
4121Earth.Animatable.prototype.animate = function( prop, val, options ) {
4122	
4123	var ani = {
4124		target: this,
4125		earth: (this.isEarth) ? this : this.earth,
4126		prop: prop,
4127		val: val,
4128		time : 0,
4129		duration : 400,
4130		relativeDuration : 0,
4131		easing : 'in-out-quad',
4132		lerpLatLng : false,
4133		loop : false,
4134		oscillate : false,
4135		paused : false
4136	};
4137	
4138	Object.assign( ani , options );
4139	
4140	
4141	// value type for lerp
4142	
4143	if ( Earth.Animatable.colorProp.indexOf( ani.prop ) != -1  ) {
4144		ani.type = 'color';
4145		ani.from = new THREE.Color( this[ ani.prop ] );
4146		ani.to = new THREE.Color( ani.val );
4147		
4148	} else if ( Earth.Animatable.latlngProp.indexOf( ani.prop ) != -1 ) {
4149		ani.type = 'latlng';
4150		ani.from = Earth.formatLatLng( Object.assign( {}, this[ ani.prop ] ) );
4151		ani.to =  Earth.formatLatLng( Object.assign( {}, ani.val ) );		
4152		
4153	} else if ( Earth.Animatable.angleProp.indexOf( ani.prop ) != -1 ) {
4154		this.options.lookAt = Earth.formatLatLng( Object.assign( {}, ani.val ) );
4155		ani.type = 'angle';
4156		ani.from = this.lookAngle;
4157		ani.to = this.getLocalAngle( this.object3d.position, this.getQuaternion(), Earth.latLngToWorld( this.options.lookAt, Earth.earthRadius ) );
4158		ani.prop = 'lookAngle';
4159
4160	} else {
4161		ani.type = 'number';
4162		ani.from = this[ ani.prop ];
4163		ani.to = ani.val;
4164		
4165	}
4166	
4167	
4168	// ani duration
4169	
4170	if ( ani.relativeDuration ) {
4171		
4172		if ( ani.type == 'number' ) {
4173			ani.duration += Math.abs( ani.from - ani.to ) * ani.relativeDuration;
4174			
4175		} else if ( ani.type == 'color' ) {
4176			ani.duration += ( Math.abs( ani.from.r - ani.to.r ) + Math.abs( ani.from.g - ani.to.g ) + Math.abs( ani.from.b - ani.to.b ) ) / 3 * ani.relativeDuration;
4177			
4178		} else if ( ani.type == 'latlng' ) {
4179			ani.duration += Earth.getDistance( ani.from, ani.to ) / 1000 * ani.relativeDuration;
4180			
4181		} else if ( ani.type == 'angle' ) {
4182			ani.duration += Math.abs( Earth.wrap( ani.from, 0, 2 * Math.PI ) - Earth.wrap( ani.to, 0, 2 * Math.PI ) ) * ani.relativeDuration;
4183			
4184		}
4185		
4186	}
4187	
4188
4189	ani.step = function( t ) {
4190			
4191		t = Earth.Animation.Easing[ this.easing ]( t );
4192		
4193		if ( this.type == 'number' ) {
4194			ani.target[ this.prop ] = THREE.Math.lerp( this.from, this.to, t );
4195			
4196		} else if ( this.type == 'color' ) {
4197			ani.target[ this.prop ] = this.from.clone().lerp( this.to, t );	
4198			
4199		} else if ( this.type == 'latlng' ) {
4200			ani.target[ this.prop ] = Earth.lerp( this.from, this.to, t, ani.lerpLatLng );
4201			
4202		} else if ( this.type == 'angle' ) {
4203			ani.target[ this.prop ] = Earth.lerpAngle( this.from, this.to, t );
4204		}
4205		
4206	};
4207	
4208	
4209	return new Earth.Animation( ani );
4210
4211};
4212
4213
4214// PUBLIC
4215// stop all animations
4216
4217Earth.Animatable.prototype.stopAllAnimations = function( dispatchComplete, jumpToEnd ) {
4218
4219	var stop_animations = [];
4220	
4221	for ( var i=0; i < this.earth.animations.length; i++ ) {
4222		if ( this.earth.animations[i].target == this ) stop_animations.push( this.earth.animations[i] );
4223	}
4224	
4225	for ( var i in stop_animations ) {
4226		stop_animations[i].stop( dispatchComplete, jumpToEnd );
4227	}
4228	
4229};
4230
4231Object.assign( Earth.prototype, Earth.Animatable.prototype );
4232Object.assign( Earth.Marker.prototype, Earth.Animatable.prototype );
4233Object.assign( Earth.Image.prototype, Earth.Animatable.prototype );
4234Object.assign( Earth.Sprite.prototype, Earth.Animatable.prototype );
4235Object.assign( Earth.Points.prototype, Earth.Animatable.prototype );
4236Object.assign( Earth.Line.prototype, Earth.Animatable.prototype );
4237Object.assign( Earth.Overlay.prototype, Earth.Animatable.prototype );
4238
4239/* <<< */
4240
4241
4242/* ORBIT */
4243// private
4244// based on THREE.OrbitControls
4245
4246Earth.Orbit = function ( camera, earth ) {
4247
4248	this.camera = camera;
4249	this.earth = earth;
4250
4251	this.enableRotate = true;
4252	this.enableZoom = true;
4253	this.zoomSpeed = 1.0;
4254
4255
4256	// How far you can dolly in and out ( PerspectiveCamera only )
4257	this.minDistance = 0;
4258	this.maxDistance = Infinity;
4259
4260	// How far you can orbit vertically, upper and lower limits.
4261	// Range is 0 to Math.PI radians.
4262	this.minPolarAngle = 0; // radians
4263	this.maxPolarAngle = Math.PI; // radians
4264
4265	// automatically rotate
4266	this.autoRotateSpeed = 0; 
4267	this.autoRotateSpeedUp = 0;
4268
4269
4270	// public methods
4271
4272	this.setPosition = function ( pos ) {
4273		
4274		scope.camera.position.copy( pos );
4275
4276		scope.camera.updateProjectionMatrix();
4277		scope.update();
4278		
4279		state = STATE.NONE;
4280		
4281		scope.dispatchEvent( changeEvent );
4282
4283	};
4284	
4285	
4286	this.cancel = function () {
4287		onMouseUp();		
4288	};
4289	
4290	
4291	this.update = function ( ) {
4292
4293		var offset = new THREE.Vector3();
4294
4295		// so camera.up is the orbit axis
4296		var quat = new THREE.Quaternion().setFromUnitVectors( camera.up, new THREE.Vector3( 0, 1, 0 ) );
4297		var quatInverse = quat.clone().invert();
4298
4299		var lastPosition = new THREE.Vector3();
4300		var lastQuaternion = new THREE.Quaternion();
4301		
4302
4303		return function update( ) {
4304
4305			var position = scope.camera.position;
4306
4307			offset.copy( position );
4308			// rotate offset to "y-axis-is-up" space
4309			offset.applyQuaternion( quat );
4310
4311			// angle from z-axis around y-axis
4312			spherical.setFromVector3( offset );
4313			
4314
4315			if ( state == STATE.NONE && scope.autoRotateSpeed != 0 ) {
4316				rotateLeft( getAutoRotationAngle() * earth.deltaTime / 30 );
4317			}
4318			
4319			if ( state == STATE.NONE && scope.autoRotateSpeedUp != 0 ) {
4320				rotateUp( getAutoRotationAngleY() * earth.deltaTime / 30 );
4321			}
4322			
4323
4324			spherical.theta += sphericalDelta.theta;
4325			spherical.phi += sphericalDelta.phi;
4326			
4327	
4328			// restrict phi to be between desired limits
4329			spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
4330
4331			spherical.makeSafe();
4332			
4333
4334			spherical.radius *= scale;
4335
4336			// restrict radius to be between desired limits
4337			spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) );
4338			
4339			
4340			offset.setFromSpherical( spherical );
4341
4342			// rotate offset back to "camera-up-vector-is-up" space
4343			offset.applyQuaternion( quatInverse );
4344			
4345
4346			position.copy( offset );
4347
4348			scope.camera.lookAt( 0, 0, 0 );
4349
4350			sphericalDelta.set( 0, 0, 0 );
4351
4352			scale = 1;
4353			
4354
4355			// has changed?
4356
4357			if ( zoomChanged ||
4358				lastPosition.distanceToSquared( scope.camera.position ) > EPS ||
4359				8 * ( 1 - lastQuaternion.dot( scope.camera.quaternion ) ) > EPS ) {
4360
4361				scope.dispatchEvent( changeEvent );
4362
4363				lastPosition.copy( scope.camera.position );
4364				lastQuaternion.copy( scope.camera.quaternion );
4365				zoomChanged = false;
4366
4367				return true;
4368
4369			}
4370
4371			return false;
4372
4373		};
4374
4375	}();
4376
4377
4378	this.dispose = function () {
4379
4380		scope.earth.canvas.removeEventListener( 'mousedown', onMouseDown, false );
4381		scope.earth.canvas.removeEventListener( 'wheel', onMouseWheel, false );
4382
4383		scope.earth.canvas.removeEventListener( 'touchstart', onTouchStart, false );
4384		scope.earth.canvas.removeEventListener( 'touchend', onTouchEnd, false );
4385		document.removeEventListener( 'touchmove', onTouchMove, false );
4386
4387		document.removeEventListener( 'mousemove', onMouseMove, false );
4388		document.removeEventListener( 'mouseup', onMouseUp, false );
4389
4390	};
4391
4392	
4393	// internals
4394
4395	var scope = this;
4396
4397	var changeEvent = { type: 'change' };
4398	var startEvent = { type: 'start' };
4399	var endEvent = { type: 'end' };
4400
4401	var STATE = { NONE: - 1, ROTATE: 0, TOUCHZOOM: 1 };
4402	var state = STATE.NONE;
4403
4404	var EPS = 0.000001;
4405
4406	// current position in spherical coordinates
4407	var spherical = new THREE.Spherical();
4408	var sphericalDelta = new THREE.Spherical();
4409
4410	var scale = 1;
4411	var zoomChanged = false;
4412
4413	var rotateStart = new THREE.Vector2();
4414	var rotateEnd = new THREE.Vector2();
4415	var rotateDelta = new THREE.Vector2();
4416	
4417	var dollyStart = new THREE.Vector2();
4418	var dollyEnd = new THREE.Vector2();
4419	var dollyDelta = new THREE.Vector2();
4420
4421	
4422	function getAutoRotationAngle() {
4423		return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
4424	}
4425	function getAutoRotationAngleY() {
4426		return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeedUp;
4427	}
4428	
4429	function getZoomScale() {
4430		return Math.pow( 0.95, scope.zoomSpeed );
4431	}
4432
4433	function rotateLeft( angle ) {
4434		sphericalDelta.theta -= angle;
4435	}
4436	function rotateUp( angle ) {
4437		sphericalDelta.phi -= angle;
4438	}
4439
4440	function dollyIn( dollyScale ) {
4441		scale /= dollyScale;
4442	}
4443	function dollyOut( dollyScale ) {
4444		scale *= dollyScale;
4445	}
4446
4447	
4448	
4449	// event callbacks - update the object state
4450
4451	function handleMouseDownRotate( event ) {
4452		
4453		rotateStart = Earth.getEventPosition( event );
4454		
4455	}
4456
4457	function handleMouseMoveRotate( event ) {
4458
4459		rotateEnd = Earth.getEventPosition( event );
4460		
4461		var earthRadiusPx = scope.earth.elementSize.y / 2 * 0.75 * scope.earth.zoom;
4462		
4463		var mouseCenterOffset = Earth.mouseCenterOffset( rotateEnd, scope.earth, earthRadiusPx );
4464		mouseCenterOffset = Earth.Animation.Easing['in-cubic']( mouseCenterOffset );
4465	
4466		var speed = 0.75 / earthRadiusPx;
4467		
4468		// compensate earth's curvature 
4469		speed *= 1 + mouseCenterOffset * 0.85;
4470		
4471		rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( speed );
4472		
4473		rotateLeft( rotateDelta.x );
4474		rotateUp( rotateDelta.y );
4475
4476		rotateStart.copy( rotateEnd );
4477		scope.update();
4478
4479	}
4480	
4481
4482	function handleMouseWheel( event ) {
4483
4484		if ( event.deltaY < 0 ) {
4485			dollyOut( getZoomScale() );
4486		} else if ( event.deltaY > 0 ) {
4487			dollyIn( getZoomScale() );
4488		}
4489
4490		scope.update();
4491
4492	}
4493
4494	
4495	function handleTouchStartDollyPan( event ) {
4496
4497		if ( scope.enableZoom ) {
4498
4499			var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
4500			var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
4501			var distance = Math.sqrt( dx * dx + dy * dy );
4502			dollyStart.set( 0, distance );
4503
4504		}
4505
4506	}
4507	
4508	function handleTouchMoveDollyPan( event ) {
4509
4510		if ( scope.enableZoom ) {
4511
4512			var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
4513			var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
4514			var distance = Math.sqrt( dx * dx + dy * dy );
4515
4516			dollyEnd.set( 0, distance );
4517			dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );
4518			dollyIn( dollyDelta.y );
4519			dollyStart.copy( dollyEnd );
4520			scope.update();
4521		
4522		}
4523
4524	}
4525
4526
4527	// event handlers
4528
4529	function onMouseDown( event ) {
4530
4531		event.preventDefault();
4532
4533		switch ( event.button ) {
4534
4535			case THREE.MOUSE.LEFT:
4536
4537				if ( scope.enableRotate === false ) return;
4538				handleMouseDownRotate( event );
4539				state = STATE.ROTATE;
4540				break;
4541
4542		}
4543
4544		if ( state !== STATE.NONE ) {
4545			document.addEventListener( 'mousemove', onMouseMove, false );
4546			document.addEventListener( 'mouseup', onMouseUp, false );
4547			scope.dispatchEvent( startEvent );
4548		}
4549
4550	}
4551
4552	function onMouseMove( event ) {
4553
4554		event.preventDefault();
4555
4556		switch ( state ) {
4557
4558			case STATE.ROTATE:
4559
4560				if ( ! scope.enableRotate ) return;
4561				handleMouseMoveRotate( event );
4562				break;
4563
4564		}
4565
4566	}
4567
4568	function onMouseUp( event ) {
4569
4570		document.removeEventListener( 'mousemove', onMouseMove, false );
4571		document.removeEventListener( 'mouseup', onMouseUp, false );
4572		scope.dispatchEvent( endEvent );
4573		state = STATE.NONE;
4574
4575	}
4576
4577	function onMouseWheel( event ) {
4578
4579		if ( ! scope.enableZoom ) return;
4580
4581		event.preventDefault();
4582		event.stopPropagation();
4583
4584		handleMouseWheel( event );
4585
4586	}
4587
4588
4589	function onTouchStart( event ) {
4590
4591		event.preventDefault();
4592
4593		switch ( event.touches.length ) {
4594
4595			case 1:	// one-fingered touch: rotate
4596
4597				if ( scope.enableRotate === false ) return;
4598				handleMouseDownRotate( event );
4599				state = STATE.ROTATE;
4600				break;
4601
4602			case 2:	// two-fingered touch: dolly-pan
4603
4604				if ( scope.enableZoom === false ) return;
4605				handleTouchStartDollyPan( event );
4606				state = STATE.TOUCHZOOM;
4607				break;
4608
4609			default:
4610				state = STATE.NONE;
4611
4612		}
4613
4614		if ( state !== STATE.NONE ) {
4615			scope.dispatchEvent( startEvent );
4616		}
4617
4618	}
4619
4620	function onTouchMove( event ) {
4621
4622		//event.preventDefault(); // removed because of chrome console warning
4623		event.stopPropagation();
4624
4625		switch ( event.touches.length ) {
4626
4627			case 1: // one-fingered touch: rotate
4628
4629				if ( ! scope.enableRotate ) return;
4630				if ( state !== STATE.ROTATE ) return; // is this needed?
4631
4632				handleMouseMoveRotate( event );
4633				break;
4634
4635			case 2: // two-fingered touch: dolly-pan
4636
4637				if ( ! scope.enableZoom ) return;
4638				if ( state !== STATE.TOUCHZOOM ) return; // is this needed?
4639
4640				handleTouchMoveDollyPan( event );
4641				break;
4642
4643			default:
4644
4645				state = STATE.NONE;
4646
4647		}
4648
4649	}
4650
4651	function onTouchEnd( event ) {
4652		
4653		scope.dispatchEvent( endEvent );
4654		state = STATE.NONE;
4655
4656	}
4657
4658	
4659	scope.earth.canvas.addEventListener( 'mousedown', onMouseDown, false );
4660	scope.earth.canvas.addEventListener( 'wheel', onMouseWheel, false );
4661
4662	scope.earth.canvas.addEventListener( 'touchstart', onTouchStart, false );
4663	scope.earth.canvas.addEventListener( 'touchend', onTouchEnd, false );
4664	document.addEventListener( 'touchmove', onTouchMove, false );
4665
4666	// force an update at start
4667	this.update();
4668
4669};
4670
4671Object.assign( Earth.Orbit.prototype, Earth.ClassicEventDispatcher.prototype );
4672
4673
4674
4675/* MeshLine */
4676// private
4677// based on THREE.MeshLine
4678
4679Earth.MeshLine = function() {
4680
4681	this.positions = [];
4682
4683	this.previous = [];
4684	this.next = [];
4685	this.side = [];
4686	this.width = [];
4687	this.indices_array = [];
4688	this.uvs = [];
4689	this.counters = [];
4690	this.geometry = new THREE.BufferGeometry();
4691
4692	this.widthCallback = null;
4693
4694};
4695
4696Earth.MeshLine.prototype.setGeometry = function( g, c ) {
4697
4698	this.widthCallback = c;
4699
4700	this.positions = [];
4701	this.counters = [];
4702
4703	if( g instanceof THREE.BufferGeometry ) {
4704		for( var j = 0; j < g.vertices.length; j++ ) {
4705			var v = g.vertices[ j ];
4706			var c = j/g.vertices.length;
4707			this.positions.push( v.x, v.y, v.z );
4708			this.positions.push( v.x, v.y, v.z );
4709			this.counters.push(c);
4710			this.counters.push(c);
4711		}
4712	}
4713
4714	if( g instanceof Float32Array || g instanceof Array ) {
4715		for( var j = 0; j < g.length; j += 3 ) {
4716			var c = j/g.length;
4717			this.positions.push( g[ j ], g[ j + 1 ], g[ j + 2 ] );
4718			this.positions.push( g[ j ], g[ j + 1 ], g[ j + 2 ] );
4719			this.counters.push(c);
4720			this.counters.push(c);
4721		}
4722	}
4723
4724	this.process();
4725
4726}
4727
4728Earth.MeshLine.prototype.compareV3 = function( a, b ) {
4729
4730	var aa = a * 6;
4731	var ab = b * 6;
4732	return ( this.positions[ aa ] === this.positions[ ab ] ) && ( this.positions[ aa + 1 ] === this.positions[ ab + 1 ] ) && ( this.positions[ aa + 2 ] === this.positions[ ab + 2 ] );
4733
4734}
4735
4736Earth.MeshLine.prototype.copyV3 = function( a ) {
4737
4738	var aa = a * 6;
4739	return [ this.positions[ aa ], this.positions[ aa + 1 ], this.positions[ aa + 2 ] ];
4740
4741}
4742
4743Earth.MeshLine.prototype.process = function() {
4744
4745	var l = this.positions.length / 6;
4746
4747	this.previous = [];
4748	this.next = [];
4749	this.side = [];
4750	this.width = [];
4751	this.indices_array = [];
4752	this.uvs = [];
4753
4754	for( var j = 0; j < l; j++ ) {
4755		this.side.push( 1 );
4756		this.side.push( -1 );
4757	}
4758
4759	var w;
4760	for( var j = 0; j < l; j++ ) {
4761		if( this.widthCallback ) w = this.widthCallback( j / ( l -1 ) );
4762		else w = 1;
4763		this.width.push( w );
4764		this.width.push( w );
4765	}
4766
4767	for( var j = 0; j < l; j++ ) {
4768		this.uvs.push( j / ( l - 1 ), 0 );
4769		this.uvs.push( j / ( l - 1 ), 1 );
4770	}
4771
4772	var v;
4773
4774	if( this.compareV3( 0, l - 1 ) ){
4775		v = this.copyV3( l - 2 );
4776	} else {
4777		v = this.copyV3( 0 );
4778	}
4779	this.previous.push( v[ 0 ], v[ 1 ], v[ 2 ] );
4780	this.previous.push( v[ 0 ], v[ 1 ], v[ 2 ] );
4781	for( var j = 0; j < l - 1; j++ ) {
4782		v = this.copyV3( j );
4783		this.previous.push( v[ 0 ], v[ 1 ], v[ 2 ] );
4784		this.previous.push( v[ 0 ], v[ 1 ], v[ 2 ] );
4785	}
4786
4787	for( var j = 1; j < l; j++ ) {
4788		v = this.copyV3( j );
4789		this.next.push( v[ 0 ], v[ 1 ], v[ 2 ] );
4790		this.next.push( v[ 0 ], v[ 1 ], v[ 2 ] );
4791	}
4792
4793	if( this.compareV3( l - 1, 0 ) ){
4794		v = this.copyV3( 1 );
4795	} else {
4796		v = this.copyV3( l - 1 );
4797	}
4798	this.next.push( v[ 0 ], v[ 1 ], v[ 2 ] );
4799	this.next.push( v[ 0 ], v[ 1 ], v[ 2 ] );
4800
4801	for( var j = 0; j < l - 1; j++ ) {
4802		var n = j * 2;
4803		this.indices_array.push( n, n + 1, n + 2 );
4804		this.indices_array.push( n + 2, n + 1, n + 3 );
4805	}
4806
4807	//if (! this.attributes) {
4808		
4809		this.attributes = {
4810			position: new THREE.BufferAttribute( new Float32Array( this.positions ), 3 ),
4811			previous: new THREE.BufferAttribute( new Float32Array( this.previous ), 3 ),
4812			next: new THREE.BufferAttribute( new Float32Array( this.next ), 3 ),
4813			side: new THREE.BufferAttribute( new Float32Array( this.side ), 1 ),
4814			width: new THREE.BufferAttribute( new Float32Array( this.width ), 1 ),
4815			uv: new THREE.BufferAttribute( new Float32Array( this.uvs ), 2 ),
4816			index: new THREE.BufferAttribute( new Uint16Array( this.indices_array ), 1 ),
4817			counters: new THREE.BufferAttribute( new Float32Array( this.counters ), 1 )
4818		};
4819		
4820		this.geometry.setAttribute( 'position', this.attributes.position );
4821		this.geometry.setAttribute( 'previous', this.attributes.previous );
4822		this.geometry.setAttribute( 'next', this.attributes.next );
4823		this.geometry.setAttribute( 'side', this.attributes.side );
4824		this.geometry.setAttribute( 'width', this.attributes.width );
4825		this.geometry.setAttribute( 'uv', this.attributes.uv );
4826		this.geometry.setIndex( this.attributes.index );
4827		this.geometry.setAttribute( 'counters', this.attributes.counters );
4828		
4829/*	} else {
4830	
4831	
4832		this.attributes.position.setArray( new Float32Array(this.positions) );
4833		this.attributes.position.needsUpdate = true;
4834		this.attributes.previous.setArray(new Float32Array(this.previous));
4835		this.attributes.previous.needsUpdate = true;
4836		this.attributes.next.setArray(new Float32Array(this.next));
4837		this.attributes.next.needsUpdate = true;
4838		this.attributes.side.setArray(new Float32Array(this.side));
4839		this.attributes.side.needsUpdate = true;
4840		this.attributes.width.setArray(new Float32Array(this.width));
4841		this.attributes.width.needsUpdate = true;
4842		this.attributes.uv.setArray(new Float32Array(this.uvs));
4843		this.attributes.uv.needsUpdate = true;
4844		this.attributes.index.setArray(new Uint16Array(this.indices_array));
4845		this.attributes.index.needsUpdate = true;
4846		this.attributes.counters.setArray(new Float32Array( this.counters ));
4847		this.attributes.counters.needsUpdate = true;
4848
4849
4850    }*/
4851
4852}
4853
4854 
4855Earth.MeshLineMat = function( parameters ) {
4856
4857	var vertexShaderSource = [
4858'precision highp float;',
4859'',
4860'attribute vec3 position;',
4861'attribute vec3 previous;',
4862'attribute vec3 next;',
4863'attribute float side;',
4864'attribute float width;',
4865'attribute vec2 uv;',
4866'attribute float counters;',
4867'',
4868'uniform mat4 projectionMatrix;',
4869'uniform mat4 modelViewMatrix;',
4870'uniform vec2 resolution;',
4871'uniform float lineWidth;',
4872'uniform vec3 color;',
4873'uniform float opacity;',
4874'uniform float near;',
4875'uniform float far;',
4876'uniform float sizeAttenuation;',
4877'',
4878'varying vec2 vUV;',
4879'varying vec4 vColor;',
4880'varying float vCounters;',
4881'',
4882'vec2 fix( vec4 i, float aspect ) {',
4883'',
4884'    vec2 res = i.xy / i.w;',
4885'    res.x *= aspect;',
4886'	 vCounters = counters;',
4887'    return res;',
4888'',
4889'}',
4890'',
4891'void main() {',
4892'',
4893'    float aspect = resolution.x / resolution.y;',
4894'	 float pixelWidthRatio = 1. / (resolution.x * projectionMatrix[0][0]);',
4895'',
4896'    vColor = vec4( color, opacity );',
4897'    vUV = uv;',
4898'',
4899'    mat4 m = projectionMatrix * modelViewMatrix;',
4900'    vec4 finalPosition = m * vec4( position, 1.0 );',
4901'    vec4 prevPos = m * vec4( previous, 1.0 );',
4902'    vec4 nextPos = m * vec4( next, 1.0 );',
4903'',
4904'    vec2 currentP = fix( finalPosition, aspect );',
4905'    vec2 prevP = fix( prevPos, aspect );',
4906'    vec2 nextP = fix( nextPos, aspect );',
4907'',
4908'	 float pixelWidth = finalPosition.w * pixelWidthRatio;',
4909'    float w = 1.8 * pixelWidth * lineWidth * width;',
4910'',
4911'    if( sizeAttenuation == 1. ) {',
4912'        w = 1.8 * lineWidth * width;',
4913'    }',
4914'',
4915'    vec2 dir;',
4916'    if( nextP == currentP ) dir = normalize( currentP - prevP );',
4917'    else if( prevP == currentP ) dir = normalize( nextP - currentP );',
4918'    else {',
4919'        vec2 dir1 = normalize( currentP - prevP );',
4920'        vec2 dir2 = normalize( nextP - currentP );',
4921'        dir = normalize( dir1 + dir2 );',
4922'',
4923'        vec2 perp = vec2( -dir1.y, dir1.x );',
4924'        vec2 miter = vec2( -dir.y, dir.x );',
4925'        //w = clamp( w / dot( miter, perp ), 0., 4. * lineWidth * width );',
4926'',
4927'    }',
4928'',
4929'    //vec2 normal = ( cross( vec3( dir, 0. ), vec3( 0., 0., 1. ) ) ).xy;',
4930'    vec2 normal = vec2( -dir.y, dir.x );',
4931'    normal.x /= aspect;',
4932'    normal *= .5 * w;',
4933'',
4934'    vec4 offset = vec4( normal * side, 0.0, 1.0 );',
4935'    finalPosition.xy += offset.xy;',
4936'',
4937'    gl_Position = finalPosition;',
4938'',
4939'}' ];
4940
4941	var fragmentShaderSource = [
4942		'#extension GL_OES_standard_derivatives : enable',
4943'precision mediump float;',
4944'',
4945'uniform sampler2D map;',
4946'uniform sampler2D alphaMap;',
4947'uniform float useMap;',
4948'uniform float useAlphaMap;',
4949'uniform float useDash;',
4950'uniform float dashArray;',
4951'uniform float dashOffset;',
4952'uniform float dashRatio;',
4953'uniform float visibility;',
4954'uniform float alphaTest;',
4955'uniform vec2 repeat;',
4956'',
4957'varying vec2 vUV;',
4958'varying vec4 vColor;',
4959'varying float vCounters;',
4960'',
4961'void main() {',
4962'',
4963'    vec4 c = vColor;',
4964'    if( useMap == 1. ) c *= texture2D( map, vUV * repeat );',
4965'    if( useAlphaMap == 1. ) c.a *= texture2D( alphaMap, vUV * repeat ).a;',
4966'    if( c.a < alphaTest ) discard;',
4967'    if( useDash == 1. ){',
4968'        c.a *= ceil(mod(vCounters + dashOffset, dashArray) - (dashArray * dashRatio));',
4969'    }',
4970'    gl_FragColor = c;',
4971'    gl_FragColor.a *= step(vCounters, visibility);',
4972'}' ];
4973
4974	function check( v, d ) {
4975		if( v === undefined ) return d;
4976		return v;
4977	}
4978
4979	//THREE.Material.call( this );
4980	
4981	parameters = parameters || {};
4982
4983	this.lineWidth = check( parameters.lineWidth, 1 );
4984	this.map = check( parameters.map, null );
4985	this.useMap = check( parameters.useMap, 0 );
4986	this.alphaMap = check( parameters.alphaMap, null );
4987	this.useAlphaMap = check( parameters.useAlphaMap, 0 );
4988	this.color = check( parameters.color, new THREE.Color( 0xffffff ) );
4989	this.opacity = check( parameters.opacity, 1 );
4990	this.resolution = check( parameters.resolution, new THREE.Vector2( 1, 1 ) );
4991	this.sizeAttenuation = check( parameters.sizeAttenuation, 1 );
4992	this.near = check( parameters.near, 1 );
4993	this.far = check( parameters.far, 1 );
4994	this.dashArray = check( parameters.dashArray, 0 );
4995	this.dashOffset = check( parameters.dashOffset, 0 );
4996	this.dashRatio = check( parameters.dashRatio, 0.5 );
4997	this.useDash = ( this.dashArray !== 0 ) ? 1 : 0;
4998	this.visibility = check( parameters.visibility, 1 );
4999	this.alphaTest = check( parameters.alphaTest, 0 );
5000	this.repeat = check( parameters.repeat, new THREE.Vector2( 1, 1 ) );
5001
5002	var material = new THREE.RawShaderMaterial( {
5003		uniforms:{
5004			lineWidth: { type: 'f', value: this.lineWidth },
5005			map: { type: 't', value: this.map },
5006			useMap: { type: 'f', value: this.useMap },
5007			alphaMap: { type: 't', value: this.alphaMap },
5008			useAlphaMap: { type: 'f', value: this.useAlphaMap },
5009			color: { type: 'c', value: this.color },
5010			opacity: { type: 'f', value: this.opacity },
5011			resolution: { type: 'v2', value: this.resolution },
5012			sizeAttenuation: { type: 'f', value: this.sizeAttenuation },
5013			near: { type: 'f', value: this.near },
5014			far: { type: 'f', value: this.far },
5015			dashArray: { type: 'f', value: this.dashArray },
5016			dashOffset: { type: 'f', value: this.dashOffset },
5017			dashRatio: { type: 'f', value: this.dashRatio },
5018			useDash: { type: 'f', value: this.useDash },
5019			visibility: {type: 'f', value: this.visibility},
5020			alphaTest: {type: 'f', value: this.alphaTest},
5021			repeat: { type: 'v2', value: this.repeat }
5022		},
5023		vertexShader: vertexShaderSource.join( '\r\n' ),
5024		fragmentShader: fragmentShaderSource.join( '\r\n' )
5025	});
5026
5027	material.type = 'MeshLineMaterial';
5028
5029	material.setValues( parameters );
5030
5031	return material;
5032
5033};
5034
5035Earth.MeshLineMat.prototype = Object.create( THREE.Material.prototype );
5036Earth.MeshLineMat.prototype.constructor = Earth.MeshLineMat;
5037
5038
5039
5040/* ObjParser */
5041// private
5042// based on THREE.OBJLoader
5043
5044Earth.ObjParser = ( function () {
5045
5046	// o object_name | g group_name
5047	var object_pattern = /^[og]\s*(.+)?/;
5048
5049	function ParserState() {
5050
5051		var state = {
5052			objects: [],
5053			object: {},
5054
5055			vertices: [],
5056			normals: [],
5057
5058			startObject: function ( name, fromDeclaration ) {
5059
5060				if ( this.object && this.object.fromDeclaration === false ) {
5061					this.object.name = name;
5062					this.object.fromDeclaration = ( fromDeclaration !== false );
5063					return;
5064				}
5065
5066				var previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined );
5067
5068				if ( this.object && typeof this.object._finalize === 'function' ) {
5069					this.object._finalize( true );
5070				}
5071
5072				this.object = {
5073					name: name || '',
5074					fromDeclaration: ( fromDeclaration !== false ),
5075
5076					geometry: {
5077						vertices: [],
5078						normals: []
5079					},
5080					materials: [],
5081					smooth: true,
5082
5083					currentMaterial: function () {
5084						if ( this.materials.length > 0 ) {
5085							return this.materials[ this.materials.length - 1 ];
5086						}
5087						return undefined;
5088					},
5089
5090					_finalize: function ( end ) {
5091
5092						var lastMultiMaterial = this.currentMaterial();
5093						if ( lastMultiMaterial && lastMultiMaterial.groupEnd === - 1 ) {
5094
5095							lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3;
5096							lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart;
5097							lastMultiMaterial.inherited = false;
5098
5099						}
5100
5101						// Ignore objects tail materials if no face declarations followed them before a new o/g started.
5102						if ( end && this.materials.length > 1 ) {
5103							for ( var mi = this.materials.length - 1; mi >= 0; mi -- ) {
5104								if ( this.materials[ mi ].groupCount <= 0 ) {
5105									this.materials.splice( mi, 1 );
5106								}
5107							}
5108						}
5109
5110						// Guarantee at least one empty material, this makes the creation later more straight forward.
5111						if ( end && this.materials.length === 0 ) {
5112							this.materials.push( {
5113								name: '',
5114								smooth: this.smooth
5115							} );
5116						}
5117
5118						return lastMultiMaterial;
5119
5120					}
5121				};
5122
5123				// Inherit previous objects material.
5124				if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function' ) {
5125					var declared = previousMaterial.clone( 0 );
5126					declared.inherited = true;
5127					this.object.materials.push( declared );
5128				}
5129
5130				this.objects.push( this.object );
5131
5132			},
5133
5134			finalize: function () {
5135				if ( this.object && typeof this.object._finalize === 'function' ) {
5136					this.object._finalize( true );
5137				}
5138			},
5139
5140			parseVertexIndex: function ( value, len ) {
5141				var index = parseInt( value, 10 );
5142				return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
5143			},
5144
5145			parseNormalIndex: function ( value, len ) {
5146				var index = parseInt( value, 10 );
5147				return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
5148			},
5149
5150			addVertex: function ( a, b, c ) {
5151				var src = this.vertices;
5152				var dst = this.object.geometry.vertices;
5153
5154				dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
5155				dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
5156				dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
5157			},
5158
5159			addNormal: function ( a, b, c ) {
5160				var src = this.normals;
5161				var dst = this.object.geometry.normals;
5162				
5163				dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
5164				dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
5165				dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
5166			},
5167
5168			addFace: function ( a, b, c, ua, ub, uc, na, nb, nc ) {
5169				var vLen = this.vertices.length;
5170
5171				var ia = this.parseVertexIndex( a, vLen );
5172				var ib = this.parseVertexIndex( b, vLen );
5173				var ic = this.parseVertexIndex( c, vLen );
5174
5175				this.addVertex( ia, ib, ic );
5176
5177
5178				if ( na !== undefined && na !== '' ) {
5179
5180					// Normals are many times the same. If so, skip function call and parseInt.
5181					var nLen = this.normals.length;
5182					ia = this.parseNormalIndex( na, nLen );
5183
5184					ib = na === nb ? ia : this.parseNormalIndex( nb, nLen );
5185					ic = na === nc ? ia : this.parseNormalIndex( nc, nLen );
5186
5187					this.addNormal( ia, ib, ic );
5188
5189				}
5190			},
5191
5192		};
5193
5194		state.startObject( '', false );
5195
5196		return state;
5197
5198	}
5199
5200
5201
5202	function OBJLoader( manager ) {
5203		
5204		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
5205		this.materials = null;
5206		
5207	}
5208
5209	OBJLoader.prototype = {
5210
5211		constructor: OBJLoader,
5212
5213		parse: function ( text ) {
5214
5215			var state = new ParserState();
5216
5217			if ( text.indexOf( '\r\n' ) !== - 1 ) {
5218				// This is faster than String.split with regex that splits on both
5219				text = text.replace( /\r\n/g, '\n' );
5220			}
5221
5222			if ( text.indexOf( '\\\n' ) !== - 1 ) {
5223				// join lines separated by a line continuation character (\)
5224				text = text.replace( /\\\n/g, '' );
5225			}
5226
5227			var lines = text.split( '\n' );
5228			var line = '', lineFirstChar = '';
5229			var lineLength = 0;
5230			var result = [];
5231
5232			// Faster to just trim left side of the line. Use if available.
5233			var trimLeft = ( typeof ''.trimLeft === 'function' );
5234
5235			for ( var i = 0, l = lines.length; i < l; i ++ ) {
5236
5237				line = lines[ i ];
5238				line = trimLeft ? line.trimLeft() : line.trim();
5239				lineLength = line.length;
5240
5241				if ( lineLength === 0 ) continue;
5242
5243				lineFirstChar = line.charAt( 0 );
5244
5245				// @todo invoke passed in handler if any
5246				if ( lineFirstChar === '#' ) continue;
5247
5248				if ( lineFirstChar === 'v' ) {
5249
5250					var data = line.split( /\s+/ );
5251
5252					switch ( data[ 0 ] ) {
5253
5254						case 'v':
5255							state.vertices.push(
5256								parseFloat( data[ 1 ] ),
5257								parseFloat( data[ 2 ] ),
5258								parseFloat( data[ 3 ] )
5259							);
5260							break;
5261							
5262						case 'vn':
5263							state.normals.push(
5264								parseFloat( data[ 1 ] ),
5265								parseFloat( data[ 2 ] ),
5266								parseFloat( data[ 3 ] )
5267							);
5268							break;
5269
5270					}
5271
5272				} else if ( lineFirstChar === 'f' ) {
5273
5274					var lineData = line.substr( 1 ).trim();
5275					var vertexData = lineData.split( /\s+/ );
5276					var faceVertices = [];
5277
5278					// Parse the face vertex data into an easy to work with format
5279
5280					for ( var j = 0, jl = vertexData.length; j < jl; j ++ ) {
5281						var vertex = vertexData[ j ];
5282
5283						if ( vertex.length > 0 ) {
5284							var vertexParts = vertex.split( '/' );
5285							faceVertices.push( vertexParts );
5286						}
5287					}
5288
5289					// Draw an edge between the first vertex and all subsequent vertices to form an n-gon
5290
5291					var v1 = faceVertices[ 0 ];
5292
5293					for ( var j = 1, jl = faceVertices.length - 1; j < jl; j ++ ) {
5294						var v2 = faceVertices[ j ];
5295						var v3 = faceVertices[ j + 1 ];
5296
5297						state.addFace(
5298							v1[ 0 ], v2[ 0 ], v3[ 0 ],
5299							v1[ 1 ], v2[ 1 ], v3[ 1 ],
5300							v1[ 2 ], v2[ 2 ], v3[ 2 ]
5301						);
5302					}
5303
5304				} else if ( ( result = object_pattern.exec( line ) ) !== null ) {
5305
5306					// o object_name or g group_name
5307					var name = ( " " + result[ 0 ].substr( 1 ).trim() ).substr( 1 );
5308					state.startObject( name );
5309
5310
5311				} else if ( lineFirstChar === 's' ) {
5312
5313					result = line.split( ' ' );
5314
5315					// smooth shading
5316					if ( result.length > 1 ) {
5317						var value = result[ 1 ].trim().toLowerCase();
5318						state.object.smooth = ( value !== '0' && value !== 'off' );
5319
5320					} else {
5321						state.object.smooth = true;
5322
5323					}
5324					
5325					var material = state.object.currentMaterial();
5326					if ( material ) material.smooth = state.object.smooth;
5327
5328				} else {
5329
5330					// Handle null terminated files
5331					if ( line === '\0' ) continue;
5332
5333				}
5334
5335			}
5336
5337			state.finalize();
5338
5339			var container = new THREE.Group();
5340
5341			for ( var i = 0, l = state.objects.length; i < l; i ++ ) {
5342
5343				var object = state.objects[ i ];
5344				var geometry = object.geometry;
5345
5346				// Skip o/g line declarations that did not follow with any faces
5347				if ( geometry.vertices.length === 0 ) continue;
5348
5349				var buffergeometry = new THREE.BufferGeometry();
5350
5351				buffergeometry.setAttribute( 'position', new THREE.Float32BufferAttribute( geometry.vertices, 3 ) );
5352
5353				if ( geometry.normals.length > 0 ) {
5354					buffergeometry.setAttribute( 'normal', new THREE.Float32BufferAttribute( geometry.normals, 3 ) );
5355				} else {
5356					buffergeometry.computeVertexNormals();
5357				}
5358
5359
5360				// Create mesh
5361
5362				var mesh = new THREE.Mesh( buffergeometry );
5363				mesh.name = object.name;
5364				container.add( mesh );
5365
5366			}
5367
5368			return container;
5369
5370		}
5371
5372	};
5373
5374	return OBJLoader;
5375
5376} )();
5377
5378
5379
5380
5381
5382/* Earth INIT */
5383
5384if ( document.readyState == 'loading' ) { // page is still loading
5385	
5386	document.addEventListener( "DOMContentLoaded", function() {
5387		Earth.dispatchLoadEvent();
5388	} );
5389	
5390} else { // async load
5391	
5392	setTimeout( function(){
5393		Earth.dispatchLoadEvent();
5394	}, 1 );
5395	
5396}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.