PageSourceSearch

https://rating.eixos.info/rating/manhattan/Qgis2threejs.js

js eixos.info collected 2026-10-09 05:01:46 UTC 113,920 bytes, 4,575 lines download raw bytes

1// (C) 2014 Minoru Akagi
2// SPDX-License-Identifier: MIT
3// https://github.com/minorua/Qgis2threejs
4
5"use strict";
6
7var Q3D = {
8
9	VERSION: "2.7.3",
10	application: {},
11	gui: {}
12
13};
14
15Q3D.Config = {
16
17	// renderer
18	renderer: {
19		hiDpi: true       // HD-DPI support
20	},
21
22	texture: {
23		anisotropy: -4    // zero means max available value. negative value means max / -v.
24	},
25
26	// scene
27	autoAdjustCameraPos: true,  // automatic camera height adjustment
28	bgColor: null,              // null is sky
29
30	// camera
31	orthoCamera: false,
32	viewpoint: {      // z-up
33		default: {      // assumed that origin is (0, 0, 0) and base extent width in 3D world coordinates is 1
34			pos: new THREE.Vector3(0, -1, 1),
35			lookAt: new THREE.Vector3()
36		}
37	},
38
39	// light
40	lights: {
41		directional: [
42			{
43				type: "ambient",
44				color: 0x999999,
45				intensity: 0.8
46			},
47			{
48				type: "directional",
49				color: 0xffffff,
50				intensity: 0.7,
51				azimuth: 220,   // azimuth of light, in degrees. default light azimuth of gdaldem hillshade is 315.
52				altitude: 45    // altitude angle in degrees.
53			}
54		],
55		point: [
56			{
57				type: "ambient",
58				color: 0x999999,
59				intensity: 0.9
60			},
61			{
62				type: "point",
63				color: 0xffffff,
64				intensity: 0.6,
65				height: 10
66			}
67		]
68	},
69
70	// layer
71	allVisible: false,   // set every layer visible property to true on load if set to true
72
73	line: {
74		dash: {
75			dashSize: 1,
76			gapSize: 0.5
77		}
78	},
79
80	label: {
81		visible: true,
82		canvasHeight: 64,
83		clickable: true
84	},
85
86	// widgets
87	navigation: {
88		enabled: true
89	},
90
91	northArrow: {
92		color: 0x8b4513,
93		cameraDistance: 30,
94		enabled: false
95	},
96
97	// animation
98	animation: {
99		enabled: false,
100		startOnLoad: false,
101		easingCurve: "Cubic",
102		repeat: false
103	},
104
105	// others
106	qmarker: {
107		radius: 0.004,
108		color: 0xffff00,
109		opacity: 0.8,
110		k: 0.2    // size factor for ortho camera
111	},
112
113	measure: {
114		marker: {
115			radius: 0.004,
116			color: 0xffff00,
117			opacity: 0.5
118			/* k: 0.2 */
119		},
120		line: {
121			color: 0xffff00
122		}
123	},
124
125	coord: {
126		visible: true,
127		latlon: false
128	},
129
130	potree: {},
131
132	debugMode: false
133};
134
135// consts
136Q3D.LayerType = {
137
138	DEM: "dem",
139	Point: "point",
140	Line: "line",
141	Polygon: "polygon",
142	PointCloud: "pc"
143
144};
145
146Q3D.MaterialType = {
147
148	MeshLambert: 0,
149	MeshPhong: 1,
150	MeshToon: 2,
151	Line: 3,
152	MeshLine: 4,
153	Sprite: 5,
154	Point: 6,
155	MeshStandard: 7,
156	Unknown: -1
157
158};
159
160Q3D.KeyframeType = {
161
162	CameraMotion: 64,
163	Opacity: 65,
164	Texture: 66,
165	GrowingLine: 67
166
167};
168
169Q3D.uv = {
170
171	i: new THREE.Vector3(1, 0, 0),
172	j: new THREE.Vector3(0, 1, 0),
173	k: new THREE.Vector3(0, 0, 1)
174
175};
176
177Q3D.deg2rad = Math.PI / 180;
178
179Q3D.ua = window.navigator.userAgent.toLowerCase();
180Q3D.isTouchDevice = ("ontouchstart" in window);
181
182Q3D.E = function (id) {
183	return document.getElementById(id);
184};
185
186
187(function () {
188
189	var app = Q3D.application,
190		gui = Q3D.gui,
191		conf = Q3D.Config,
192		E = Q3D.E;
193
194	var vec3 = new THREE.Vector3();
195
196	/*
197	Q3D.application
198	*/
199	var listeners = {};
200	app.dispatchEvent = function (event) {
201		var ls = listeners[event.type] || [];
202		for (var i = 0; i < ls.length; i++) {
203			ls[i](event);
204		}
205	};
206
207	app.addEventListener = function (type, listener, prepend) {
208		listeners[type] = listeners[type] || [];
209		if (prepend) {
210			listeners[type].unshift(listener);
211		}
212		else {
213			listeners[type].push(listener);
214		}
215	};
216
217	app.removeEventListener = function (type, listener) {
218		var array = listeners[type];
219		if (array !== undefined) {
220			var idx = array.indexOf(listener);
221			if (idx !== -1) array.splice(idx, 1);
222		}
223	};
224
225	app.init = function (container) {
226
227		app.container = container;
228
229		app.selectedObject = null;
230		app.highlightObject = null;
231
232		app.modelBuilders = [];
233		app._wireframeMode = false;
234
235		// URL parameters
236		var params = app.parseUrlParameters();
237		app.urlParams = params;
238
239		if ("popup" in params) {
240			// open popup window
241			var c = window.location.href.split("?");
242			window.open(c[0] + "?" + c[1].replace(/&?popup/, ""), "popup", "width=" + params.width + ",height=" + params.height);
243			gui.popup.show("Another window has been opened.");
244			return;
245		}
246
247		if (params.hiDpi == "no") conf.renderer.hiDpi = false;
248		if (params.anisotropy) conf.texture.anisotropy = parseFloat(params.anisotropy);
249
250		if (params.cx !== undefined) conf.viewpoint.pos = new THREE.Vector3(parseFloat(params.cx), parseFloat(params.cy), parseFloat(params.cz));
251		if (params.tx !== undefined) conf.viewpoint.lookAt  = new THREE.Vector3(parseFloat(params.tx), parseFloat(params.ty), parseFloat(params.tz));
252
253		if (params.width && params.height) {
254			container.style.width = params.width + "px";
255			container.style.height = params.height + "px";
256		}
257
258		app.width = container.clientWidth;
259		app.height = container.clientHeight;
260
261		var bgcolor = conf.bgColor;
262		if (bgcolor === null) container.classList.add("sky");
263
264		// WebGLRenderer
265		app.renderer = new THREE.WebGLRenderer({alpha: true, antialias: true});
266
267		if (conf.renderer.hiDpi) {
268			app.renderer.setPixelRatio(window.devicePixelRatio);
269		}
270
271		app.renderer.setSize(app.width, app.height);
272		app.renderer.setClearColor(bgcolor || 0, (bgcolor === null) ? 0 : 1);
273		app.container.appendChild(app.renderer.domElement);
274
275		if (conf.texture.anisotropy <= 0) {
276			var maxAnis = app.renderer.capabilities.getMaxAnisotropy() || 1;
277
278			if (conf.texture.anisotropy == 0) {
279				conf.texture.anisotropy = maxAnis;
280			}
281			else {
282				conf.texture.anisotropy = (maxAnis > -conf.texture.anisotropy) ? -maxAnis / conf.texture.anisotropy : 1;
283			}
284		}
285
286		// outline effect
287		if (THREE.OutlineEffect !== undefined) app.effect = new THREE.OutlineEffect(app.renderer);
288
289		// scene
290		app.scene = new Q3DScene();
291
292		app.scene.addEventListener("renderRequest", function (event) {
293			app.render();
294		});
295
296		app.scene.addEventListener("cameraUpdateRequest", function (event) {
297			app.camera.position.copy(event.pos);
298			app.camera.lookAt(event.focal);
299			if (app.controls.target !== undefined) app.controls.target.copy(event.focal);
300			if (app.controls.saveState !== undefined) app.controls.saveState();
301
302			if (event.far !== undefined) {
303				app.camera.near = (app.camera.isOrthographicCamera) ? 0 : event.near;
304				app.camera.far = event.far;
305				app.camera.updateProjectionMatrix();
306			}
307		});
308
309		app.scene.addEventListener("lightChanged", function (event) {
310			if (event.light == "point") {
311				app.scene.add(app.camera);
312				app.camera.add(app.scene.lightGroup);
313			}
314			else {    // directional
315				app.scene.remove(app.camera);
316				app.scene.add(app.scene.lightGroup);
317			}
318		});
319
320		app.scene.addEventListener("mapRotationChanged", function (event) {
321			if (app.scene2) {
322				app.scene2.lightGroup.clear();
323				app.scene2.buildLights(Q3D.Config.lights.directional, event.rotation);
324			}
325		});
326
327		// camera
328		app.buildCamera(conf.orthoCamera);
329
330		// controls
331		if (THREE.OrbitControls) {
332			app.controls = new THREE.OrbitControls(app.camera, app.renderer.domElement);
333
334			app.controls.addEventListener("change", function (event) {
335				app.render();
336			});
337
338			app.controls.update();
339		}
340
341		// navigation
342		if (conf.navigation.enabled && typeof ViewHelper !== "undefined") {
343			app.buildViewHelper(E("navigation"));
344		}
345
346		// north arrow
347		if (conf.northArrow.enabled) {
348			app.buildNorthArrow(E("northarrow"));
349		}
350
351		// labels
352		app.labelVisible = conf.label.visible;
353
354		// create a marker for queried point
355		var opt = conf.qmarker;
356		app.queryMarker = new THREE.Mesh(new THREE.SphereBufferGeometry(opt.radius, 32, 32),
357										 new THREE.MeshLambertMaterial({color: opt.color, opacity: opt.opacity, transparent: (opt.opacity < 1)}));
358		app.queryMarker.name = "marker";
359
360		app.queryMarker.onBeforeRender = function (renderer, scene, camera, geometry, material, group) {
361			this.scale.setScalar(this.position.distanceTo(camera.position) * ((camera.isPerspectiveCamera) ? 1 : conf.qmarker.k));
362			this.updateMatrixWorld();
363		};
364
365		app.highlightMaterial = new THREE.MeshLambertMaterial({emissive: 0x999900, transparent: true, opacity: 0.5, side: THREE.DoubleSide});
366
367		// loading manager
368		app.initLoadingManager();
369
370		// event listeners
371		app.addEventListener("sceneLoaded", function () {
372			if (conf.viewpoint.pos === undefined && conf.autoAdjustCameraPos) {
373				app.adjustCameraPosition();
374			}
375			app.render();
376
377			if (conf.animation.enabled) {
378				var btn = E("animbtn");
379				if (btn) {
380					btn.className = "playbtn";
381				}
382
383				if (conf.animation.startOnLoad) {
384					app.animation.keyframes.start();
385				}
386			}
387		}, true);
388
389		window.addEventListener("keydown", app.eventListener.keydown);
390		window.addEventListener("resize", app.eventListener.resize);
391
392		app.renderer.domElement.addEventListener("mousedown", app.eventListener.mousedown);
393		app.renderer.domElement.addEventListener("mouseup", app.eventListener.mouseup);
394
395		gui.init();
396	};
397
398	app.parseUrlParameters = function () {
399		var p, vars = {};
400		var params = window.location.search.substring(1).split('&').concat(window.location.hash.substring(1).split('&'));
401		params.forEach(function (param) {
402			p = param.split('=');
403			vars[p[0]] = p[1];
404		});
405		return vars;
406	};
407
408	app.initLoadingManager = function () {
409		if (app.loadingManager) {
410			app.loadingManager.onLoad = app.loadingManager.onProgress = app.loadingManager.onError = undefined;
411		}
412
413		app.loadingManager = new THREE.LoadingManager(function () {   // onLoad
414			app.loadingManager.isLoading = false;
415
416			E("progressbar").classList.add("fadeout");
417
418			app.dispatchEvent({type: "sceneLoaded"});
419		},
420		function (url, loaded, total) {   // onProgress
421			E("progressbar").style.width = (loaded / total * 100) + "%";
422		},
423		function () {   // onError
424			app.loadingManager.isLoading = false;
425
426			app.dispatchEvent({type: "sceneLoadError"});
427		});
428
429		app.loadingManager.onStart = function () {
430			app.loadingManager.isLoading = true;
431		};
432
433		app.loadingManager.isLoading = false;
434	};
435
436	app.loadFile = function (url, type, callback) {
437
438		var loader = new THREE.FileLoader(app.loadingManager);
439		loader.setResponseType(type);
440
441		var onError = function (e) {
442			if (location.protocol == "file:") {
443				gui.popup.show("This browser doesn't allow loading local files via Ajax. See <a href='https://github.com/minorua/Qgis2threejs/wiki/Browser-Support'>plugin wiki page</a> for details.", "Error", true);
444			}
445		};
446
447		try {
448			loader.load(url, callback, undefined, onError);
449		}
450		catch (e) {      // for IE
451			onError(e);
452		}
453	};
454
455	app.loadJSONObject = function (jsonObject) {
456		app.scene.loadJSONObject(jsonObject);
457		if (jsonObject.animation !== undefined) app.animation.keyframes.load(jsonObject.animation.groups);
458	};
459
460	app.loadJSONFile = function (url, callback) {
461		app.loadFile(url, "json", function (obj) {
462			app.loadJSONObject(obj);
463			if (callback) callback(obj);
464		});
465	};
466
467	app.loadSceneFile = function (url, sceneFileLoadedCallback, sceneLoadedCallback) {
468
469		var onload = function () {
470			if (sceneFileLoadedCallback) sceneFileLoadedCallback(app.scene);
471		};
472
473		if (sceneLoadedCallback) {
474			app.addEventListener("sceneLoaded", function () {
475				sceneLoadedCallback(app.scene);
476			});
477		}
478
479		var ext = url.split(".").pop();
480		if (ext == "json") app.loadJSONFile(url, onload);
481		else if (ext == "js") {
482			var e = document.createElement("script");
483			e.src = url;
484			e.onload = onload;
485			document.body.appendChild(e);
486		}
487	};
488
489	app.loadTextureFile = function (url, callback) {
490		return new THREE.TextureLoader(app.loadingManager).load(url, callback);
491	};
492
493	app.loadModelFile = function (url, callback) {
494		var loader,
495			ext = url.split(".").pop();
496
497		if (ext == "dae") {
498			loader = new THREE.ColladaLoader(app.loadingManager);
499		}
500		else if (ext == "gltf" || ext == "glb") {
501			loader = new THREE.GLTFLoader(app.loadingManager);
502		}
503		else {
504			console.log("Model file type not supported: " + url);
505			return;
506		}
507
508		app.loadingManager.itemStart("M" + url);
509
510		loader.load(url, function (model) {
511			if (callback) callback(model);
512			app.loadingManager.itemEnd("M" + url);
513		},
514		undefined,
515		function (e) {
516			console.log("Failed to load model: " + url);
517			app.loadingManager.itemError("M" + url);
518		});
519	};
520
521	app.loadModelData = function (data, ext, resourcePath, callback) {
522
523		if (ext == "dae") {
524			var model = new THREE.ColladaLoader(app.loadingManager).parse(data, resourcePath);
525			if (callback) callback(model);
526		}
527		else if (ext == "gltf" || ext == "glb") {
528			new THREE.GLTFLoader(app.loadingManager).parse(data, resourcePath, function (model) {
529				if (callback) callback(model);
530			}, function (e) {
531				console.log("Failed to load a glTF model: " + e);
532			});
533		}
534		else {
535			console.log("Model file type not supported: " + ext);
536			return;
537		}
538	};
539
540	app.mouseDownPoint = new THREE.Vector2();
541	app.mouseUpPoint = new THREE.Vector2();
542
543	app.eventListener = {
544
545		keydown: function (e) {
546			if (e.ctrlKey) return;
547
548			if (e.shiftKey) {
549				switch (e.keyCode) {
550					case 82:  // Shift + R
551						app.controls.reset();
552						return;
553					case 83:  // Shift + S
554						gui.showPrintDialog();
555						return;
556				}
557				return;
558			}
559
560			switch (e.keyCode) {
561				case 8:   // BackSpace
562					if (app.measure.isActive) app.measure.removeLastPoint();
563					return;
564				case 13:  // Enter
565					app.animation.keyframes.resume();
566					return;
567				case 27:  // ESC
568					if (gui.popup.isVisible()) {
569						app.cleanView();
570					}
571					else if (app.controls.autoRotate) {
572						app.setRotateAnimationMode(false);
573					}
574					return;
575				case 73:  // I
576					gui.showInfo();
577					return;
578				case 76:  // L
579					app.setLabelVisible(!app.labelVisible);
580					return;
581				case 82:  // R
582					app.setRotateAnimationMode(!app.controls.autoRotate);
583					return;
584				case 87:  // W
585					app.setWireframeMode(!app._wireframeMode);
586					return;
587			}
588		},
589
590		mousedown: function (e) {
591			app.mouseDownPoint.set(e.clientX, e.clientY);
592		},
593
594		mouseup: function (e) {
595			app.mouseUpPoint.set(e.clientX, e.clientY);
596			if (app.mouseDownPoint.equals(app.mouseUpPoint)) app.canvasClicked(e);
597		},
598
599		resize: function () {
600			app.setCanvasSize(app.container.clientWidth, app.container.clientHeight);
601			app.render();
602		}
603
604	};
605
606	app.setCanvasSize = function (width, height) {
607		var changed = (app.width != width || app.height != height);
608
609		app.width = width;
610		app.height = height;
611		app.camera.aspect = width / height;
612		app.camera.updateProjectionMatrix();
613		app.renderer.setSize(width, height);
614
615		if (changed) app.dispatchEvent({type: "canvasSizeChanged"});
616	};
617
618	app.buildCamera = function (is_ortho) {
619		if (is_ortho) {
620			app.camera = new THREE.OrthographicCamera(-app.width / 10, app.width / 10, app.height / 10, -app.height / 10);
621		}
622		else {
623			app.camera = new THREE.PerspectiveCamera(45, app.width / app.height);
624		}
625
626		// magic to change y-up world to z-up
627		app.camera.up.set(0, 0, 1);
628
629		var be = app.scene.userData.baseExtent;
630		if (be) {
631			app.camera.near = (is_ortho) ? 0 : 0.001 * be.width;
632			app.camera.far = 100 * be.width;
633			app.camera.updateProjectionMatrix();
634		}
635	};
636
637	// move camera target to center of scene
638	app.adjustCameraPosition = function (force) {
639		if (!force) {
640			app.render();
641
642			// stay at current position if rendered objects exist
643			var r = app.renderer.info.render;
644			if (r.triangles + r.points + r.lines) return;
645		}
646		var bbox = app.scene.boundingBox();
647		if (bbox.isEmpty()) return;
648
649		bbox.getCenter(vec3);
650		app.cameraAction.zoom(vec3.x, vec3.y, (bbox.max.z + vec3.z) / 2, app.scene.userData.baseExtent.width);
651	};
652
653	// declination: clockwise from +y, in degrees
654	app.buildNorthArrow = function (container, declination) {
655		container.style.display = "block";
656
657		app.renderer2 = new THREE.WebGLRenderer({alpha: true, antialias: true});
658		app.renderer2.setClearColor(0, 0);
659		app.renderer2.setSize(container.clientWidth, container.clientHeight);
660
661		app.container2 = container;
662		app.container2.appendChild(app.renderer2.domElement);
663
664		app.camera2 = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 1, 1000);
665		app.camera2.position.set(0, 0, conf.northArrow.cameraDistance);
666		app.camera2.up = app.camera.up;
667
668		app.scene2 = new Q3DScene();
669		app.scene2.buildLights(conf.lights.directional, 0);
670
671		// an arrow object
672		var geometry = new THREE.Geometry();
673		geometry.vertices.push(
674			new THREE.Vector3(-5, -10, 0),
675			new THREE.Vector3(0, 10, 0),
676			new THREE.Vector3(0, -7, 3),
677			new THREE.Vector3(5, -10, 0)
678		);
679		geometry.faces.push(
680			new THREE.Face3(0, 1, 2),
681			new THREE.Face3(2, 1, 3)
682		);
683		geometry.computeFaceNormals();
684
685		var material = new THREE.MeshLambertMaterial({color: conf.northArrow.color, side: THREE.DoubleSide});
686		var mesh = new THREE.Mesh(geometry, material);
687		if (declination) mesh.rotation.z = -declination * Q3D.deg2rad;
688		app.scene2.add(mesh);
689	};
690
691	app.buildViewHelper = function (container) {
692
693		if (app.renderer3 === undefined) {
694			container.style.display = "block";
695
696			app.renderer3 = new THREE.WebGLRenderer({alpha: true, antialias: true});
697			app.renderer3.setClearColor(0, 0);
698			app.renderer3.setSize(container.clientWidth, container.clientHeight);
699
700			app.container3 = container;
701			app.container3.appendChild(app.renderer3.domElement);
702		}
703
704		if (app.viewHelper !== undefined) {
705			app.viewHelper.removeEventListener("requestAnimation", app.startViewHelperAnimation);
706		}
707
708		app.viewHelper = new ViewHelper(app.camera, {dom: container});
709		app.viewHelper.controls = app.controls;
710
711		app.viewHelper.addEventListener("requestAnimation", app.startViewHelperAnimation);
712	};
713
714	var clock = new THREE.Clock();
715	app.startViewHelperAnimation = function () {
716		clock.start();
717		requestAnimationFrame(app.animate);
718	};
719
720	app.currentViewUrl = function () {
721		var c = app.scene.toMapCoordinates(app.camera.position),
722			t = app.scene.toMapCoordinates(app.controls.target),
723			hash = "#cx=" + c.x.toFixed(3) + "&cy=" + c.y.toFixed(3) + "&cz=" + c.z.toFixed(3);
724		if (t.x || t.y || t.z) hash += "&tx=" + t.x.toFixed(3) + "&ty=" + t.y.toFixed(3) + "&tz=" + t.z.toFixed(3);
725		return window.location.href.split("#")[0] + hash;
726	};
727
728	// enable the controls
729	app.start = function () {
730		if (app.controls) app.controls.enabled = true;
731	};
732
733	app.pause = function () {
734		app.animation.isActive = false;
735		if (app.controls) app.controls.enabled = false;
736	};
737
738	app.resume = function () {
739		if (app.controls) app.controls.enabled = true;
740	};
741
742	// animation loop
743	app.animate = function () {
744
745		if (app.animation.isActive) {
746			requestAnimationFrame(app.animate);
747
748			if (app.animation.keyframes.isActive) TWEEN.update();
749			else if (app.controls.enabled) app.controls.update();
750		}
751		else if (app.viewHelper && app.viewHelper.animating) {
752			requestAnimationFrame(app.animate);
753
754			app.viewHelper.update(clock.getDelta());
755		}
756
757		app.render();
758	};
759
760	app.animation = {
761
762		isActive: false,
763
764		start: function () {
765			this.isActive = true;
766			app.animate();
767		},
768
769		stop: function () {
770			this.isActive = false;
771		},
772
773		keyframes: {    // keyframe animation
774
775			isActive: false,
776
777			isPaused: false,
778
779			curveFactor: 0,
780
781			easingFunction: function (easing) {
782				if (easing == 1) return TWEEN.Easing.Linear.None;
783				if (easing > 1) {
784					var f = TWEEN.Easing[Q3D.Config.animation.easingCurve];
785					if (easing == 2) return f["InOut"];
786					else if (easing == 3) return f["In"];
787					else return f["Out"];   // easing == 4
788				}
789			},
790
791			keyframeGroups: [],
792
793			clear: function () {
794				this.keyframeGroups = [];
795			},
796
797			load: function (group) {
798				if (!Array.isArray(group)) group = [group];
799
800				this.keyframeGroups = this.keyframeGroups.concat(group);
801			},
802
803			start: function () {
804
805				var _this = this,
806					e = E("narrativebox"),
807					btn = E("nextbtn"),
808					currentNarElem;
809
810				this.keyframeGroups.forEach(function (group) {
811
812					var t;
813					for (var p in Q3D.Tweens) {
814						if (Q3D.Tweens[p].type == group.type) {
815							t = Q3D.Tweens[p];
816							break;
817						}
818					}
819					if (t === undefined) {
820						console.warn("unknown animation type: " + group.type);
821						return;
822					}
823
824					var layer = (group.layerId !== undefined) ? app.scene.mapLayers[group.layerId] : undefined;
825
826					group.completed = false;
827					group.currentIndex = 0;
828					group.prop_list = [];
829
830					t.init(group, layer);
831
832					var keyframes = group.keyframes;
833
834					var showNBox = function (idx) {
835						// narrative box
836						var n = keyframes[idx].narration;
837						if (n && e) {
838							if (currentNarElem) {
839								currentNarElem.classList.remove("visible");
840							}
841
842							currentNarElem = E(n.id);
843							if (currentNarElem) {
844								currentNarElem.classList.add("visible");
845							}
846							else {    // preview
847								E("narbody").innerHTML = n.text;
848							}
849
850							if (btn) {
851								if (idx < keyframes.length - 1) {
852									btn.className = "nextbtn";
853									btn.innerHTML =  "";
854								}
855								else {
856									btn.className = "";
857									btn.innerHTML = "Close";
858								}
859							}
860
861							setTimeout(function () {
862								_this.pause();
863								e.classList.add("visible");
864							}, 0);
865						}
866					};
867
868					var onStart = function () {
869						if (group.onStart) group.onStart();
870
871						app.dispatchEvent({type: "tweenStarted", index: group.currentIndex});
872
873						// pause if narrative box is shown
874						if (e && e.classList.contains("visible")) {
875							e.classList.remove("visible");
876						}
877					};
878
879					var onComplete = function (obj) {
880						if (!keyframes[group.currentIndex].easing) {
881							group.onUpdate(obj, 1);
882						}
883
884						if (group.onComplete) group.onComplete(obj);
885
886						var index = ++group.currentIndex;
887						if (index == keyframes.length - 1) {
888							group.completed = true;
889
890							var completed = true;
891							for (var i = 0; i < _this.keyframeGroups.length; i++) {
892								if (!_this.keyframeGroups[i].completed) completed = false;
893							}
894
895							if (completed) {
896								if (currentNarElem) {
897									currentNarElem.classList.remove("visible");
898								}
899
900								if (conf.animation.repeat) {
901									setTimeout(function () {
902										_this.start();
903									}, 0);
904								}
905								else {
906									_this.stop();
907								}
908							}
909						}
910
911						// show narrative box if the current keyframe has a narrative content
912						showNBox(index);
913					};
914
915					var t0, t1, t2;
916					for (var i = 0; i < keyframes.length - 1; i++) {
917
918						t2 = new TWEEN.Tween(group.prop_list[i]).delay(keyframes[i].delay).onStart(onStart)
919										 .to(group.prop_list[i + 1], keyframes[i].duration).onComplete(onComplete);
920
921						if (keyframes[i].easing) {
922							t2.easing(_this.easingFunction(keyframes[i].easing)).onUp
922date(group.onUpdate);
923						}
924
925						if (i == 0) {
926							t0 = t2;
927						}
928						else {
929							t1.chain(t2);
930						}
931						t1 = t2;
932					}
933
934					showNBox(0);
935
936					t0.start();
937				});
938
939				app.animation.isActive = this.isActive = true;
940				app.dispatchEvent({type: "animationStarted"});
941				app.animate();
942			},
943
944			stop: function () {
945
946				TWEEN.removeAll();
947
948				app.animation.isActive = this.isActive = this.isPaused = false;
949				this._pausedTweens = null;
950
951				app.dispatchEvent({type: "animationStopped"});
952			},
953
954			pause: function () {
955
956				if (this.isPaused) return;
957
958				this._pausedTweens = TWEEN.getAll();
959
960				if (this._pausedTweens.length) {
961					for (var i = 0; i < this._pausedTweens.length; i++) {
962						this._pausedTweens[i].pause();
963					}
964					this.isPaused = true;
965				}
966				app.animation.isActive = this.isActive = false;
967			},
968
969			resume: function () {
970
971				var box = E("narrativebox");
972				if (box && box.classList.contains("visible")) {
973					box.classList.remove("visible");
974				}
975
976				if (!this.isPaused) return;
977
978				for (var i = 0; i < this._pausedTweens.length; i++) {
979					this._pausedTweens[i].resume();
980				}
981				this._pausedTweens = null;
982
983				app.animation.isActive = this.isActive = true;
984				this.isPaused = false;
985
986				app.animate();
987			}
988		},
989
990		orbit: {      // orbit animation
991
992			isActive: false,
993
994			start: function () {
995
996				app.controls.autoRotate = true;
997				app.animation.isActive = this.isActive = true;
998
999				app.animate();
1000			},
1001
1002			stop: function () {
1003
1004				app.controls.autoRotate = false;
1005				app.animation.isActive = this.isActive = false;
1006			}
1007		}
1008	};
1009
1010	app.render = function (updateControls) {
1011		if (updateControls) {
1012			app.controls.update();
1013		}
1014
1015		if (app.camera.parent) {
1016			app.camera.updateMatrixWorld();
1017		}
1018
1019		// render
1020		if (app.effect) {
1021			app.effect.render(app.scene, app.camera);
1022		}
1023		else {
1024			app.renderer.render(app.scene, app.camera);
1025		}
1026
1027		// North arrow
1028		if (app.renderer2) {
1029			app.scene2.quaternion.copy(app.camera.quaternion).inverse();
1030			app.scene2.updateMatrixWorld();
1031
1032			app.renderer2.render(app.scene2, app.camera2);
1033		}
1034
1035		// navigation widget
1036		if (app.viewHelper) {
1037			app.viewHelper.render(app.renderer3);
1038		}
1039	};
1040
1041	(function () {
1042		var dly, rpt, times, id = null;
1043		var func = function () {
1044			app.render();
1045			if (rpt <= ++times) {
1046				clearInterval(id);
1047				id = null;
1048			}
1049		};
1050		app.setIntervalRender = function (delay, repeat) {
1051			if (id === null || delay != dly) {
1052				if (id !== null) {
1053					clearInterval(id);
1054				}
1055				id = setInterval(func, delay);
1056				dly = delay;
1057			}
1058			rpt = repeat;
1059			times = 0;
1060		};
1061	})();
1062
1063	app.setLabelVisible = function (visible) {
1064		app.labelVisible = visible;
1065		app.scene.labelGroup.visible = visible;
1066		app.scene.labelConnectorGroup.visible = visible;
1067		app.render();
1068	};
1069
1070	app.setRotateAnimationMode = function (enabled) {
1071		if (enabled) {
1072			app.animation.orbit.start();
1073		}
1074		else {
1075			app.animation.orbit.stop();
1076		}
1077	};
1078
1079	app.setWireframeMode = function (wireframe) {
1080		if (wireframe == app._wireframeMode) return;
1081
1082		for (var id in app.scene.mapLayers) {
1083			app.scene.mapLayers[id].setWireframeMode(wireframe);
1084		}
1085
1086		app._wireframeMode = wireframe;
1087		app.render();
1088	};
1089
1090	app.intersectObjects = function (offsetX, offsetY) {
1091		var vec2 = new THREE.Vector2((offsetX / app.width) * 2 - 1,
1092								    -(offsetY / app.height) * 2 + 1);
1093		var ray = new THREE.Raycaster();
1094		ray.linePrecision = 0.2;
1095		ray.setFromCamera(vec2, app.camera);
1096		return ray.intersectObjects(app.scene.visibleObjects(app.labelVisible));
1097	};
1098
1099	app._offset = function (elm) {
1100		var top = 0, left = 0;
1101		do {
1102			top += elm.offsetTop || 0; left += elm.offsetLeft || 0; elm = elm.offsetParent;
1103		} while (elm);
1104		return {top: top, left: left};
1105	};
1106
1107	app.queryTargetPosition = new THREE.Vector3();
1108
1109	app.cameraAction = {
1110
1111		move: function (x, y, z) {
1112			if (x === undefined) app.camera.position.copy(app.queryTargetPosition);
1113			else app.camera.position.set(x, y, z);
1114
1115			app.render(true);
1116			app.cleanView();
1117		},
1118
1119		vecZoom: new THREE.Vector3(0, -1, 1).normalize(),
1120
1121		zoom: function (x, y, z, dist) {
1122			if (x === undefined) vec3.copy(app.queryTargetPosition);
1123			else vec3.set(x, y, z);
1124
1125			if (dist === undefined) dist = app.scene.userData.baseExtent.width * 0.1;
1126
1127			app.camera.position.copy(app.cameraAction.vecZoom).multiplyScalar(dist).add(vec3);
1128			app.camera.lookAt(vec3);
1129			if (app.controls.target !== undefined) app.controls.target.copy(vec3);
1130			app.render(true);
1131			app.cleanView();
1132		},
1133
1134		zoomToLayer: function (layer) {
1135			if (!layer) return;
1136
1137			var bbox = layer.boundingBox();
1138
1139			bbox.getSize(vec3);
1140			var dist = Math.max(vec3.x, vec3.y * 3 / 4) * 1.2;
1141
1142			bbox.getCenter(vec3);
1143			app.cameraAction.zoom(vec3.x, vec3.y, vec3.z, dist);
1144		},
1145
1146		orbit: function (x, y, z) {
1147			if (app.controls.target === undefined) return;
1148
1149			if (x === undefined) app.controls.target.copy(app.queryTargetPosition);
1150			else app.controls.target.set(x, y, z);
1151
1152			app.setRotateAnimationMode(true);
1153			app.cleanView();
1154		}
1155
1156	};
1157
1158	app.cleanView = function () {
1159		gui.clean();
1160
1161		app.scene.remove(app.queryMarker);
1162		app.highlightFeature(null);
1163		app.measure.clear();
1164		app.render();
1165
1166		app.selectedLayer = null;
1167
1168		if (app._canvasImageUrl) {
1169			URL.revokeObjectURL(app._canvasImageUrl);
1170			app._canvasImageUrl = null;
1171		}
1172	};
1173
1174	app.highlightFeature = function (object) {
1175		if (app.highlightObject) {
1176			// remove highlight object from the scene
1177			app.scene.remove(app.highlightObject);
1178			app.selectedObject = null;
1179			app.highlightObject = null;
1180		}
1181
1182		if (object === null) return;
1183
1184		var layer = app.scene.mapLayers[object.userData.layerId];
1185		if (!layer || layer.type == Q3D.LayerType.DEM || layer.type == Q3D.LayerType.PointCloud) return;
1186		if (layer.properties.objType == "Billboard") return;
1187
1188		// create a highlight object (if layer type is Point, slightly bigger than the object)
1189		var s = (layer.type == Q3D.LayerType.Point) ? 1.01 : 1;
1190
1191		var clone = object.clone();
1192		clone.traverse(function (obj) {
1193			obj.material = app.highlightMaterial;
1194		});
1195		if (s != 1) clone.scale.multiplyScalar(s);
1196
1197		// add the highlight object to the scene
1198		app.scene.add(clone);
1199
1200		app.selectedObject = object;
1201		app.highlightObject = clone;
1202	};
1203
1204	app.canvasClicked = function (e) {
1205
1206		// button 2: right click
1207		if (e.button == 2 && app.measure.isActive) {
1208			app.measure.removeLastPoint();
1209			return;
1210		}
1211
1212		var canvasOffset = app._offset(app.renderer.domElement);
1213		var objs = app.intersectObjects(e.clientX - canvasOffset.left, e.clientY - canvasOffset.top);
1214
1215		var obj, o, layer, layerId;
1216		for (var i = 0, l = objs.length; i < l; i++) {
1217			obj = objs[i];
1218
1219			if (app.measure.isActive) {
1220				app.measure.addPoint(obj.point);
1221				return;
1222			}
1223
1224			// get layerId of clicked object
1225			o = obj.object;
1226			while (o) {
1227				layerId = o.userData.layerId;
1228				if (layerId !== undefined) break;
1229				o = o.parent;
1230			}
1231
1232			if (layerId === undefined) break;
1233
1234			layer = app.scene.mapLayers[layerId];
1235			if (!layer.clickable) break;
1236
1237			app.selectedLayer = layer;
1238			app.queryTargetPosition.copy(obj.point);
1239
1240			// query marker
1241			app.queryMarker.position.copy(obj.point);
1242			app.scene.add(app.queryMarker);
1243
1244			if (o.userData.isLabel) {
1245				o = o.userData.objs[o.userData.partIdx];    // label -> object
1246			}
1247
1248			app.highlightFeature(o);
1249			app.render();
1250			gui.showQueryResult(obj.point, layer, o, conf.coord.visible);
1251
1252			return;
1253		}
1254		if (app.measure.isActive) return;
1255
1256		app.cleanView();
1257
1258		if (app.controls.autoRotate) {
1259			app.setRotateAnimationMode(false);
1260		}
1261	};
1262
1263	app.saveCanvasImage = function (width, height, fill_background, saveImageFunc) {
1264		if (fill_background === undefined) fill_background = true;
1265
1266		// set canvas size
1267		var old_size;
1268		if (width && height) {
1269			old_size = [app.width, app.height];
1270			app.setCanvasSize(width, height);
1271		}
1272
1273		// functions
1274		var saveBlob = function (blob) {
1275			var filename = "image.png";
1276
1277			// ie
1278			if (window.navigator.msSaveBlob !== undefined) {
1279				window.navigator.msSaveBlob(blob, filename);
1280				gui.popup.hide();
1281			}
1282			else {
1283				// create object url
1284				if (app._canvasImageUrl) URL.revokeObjectURL(app._canvasImageUrl);
1285				app._canvasImageUrl = URL.createObjectURL(blob);
1286
1287				// display a link to save the image
1288				var e = document.createElement("a");
1289				e.className = "download-link";
1290				e.href = app._canvasImageUrl;
1291				e.download = filename;
1292				e.innerHTML = "Save";
1293				gui.popup.show("Click to save the image to a file." + e.outerHTML, "Image is ready");
1294			}
1295		};
1296
1297		var saveCanvasImage = saveImageFunc || function (canvas) {
1298			if (canvas.toBlob !== undefined) {
1299				canvas.toBlob(saveBlob);
1300			}
1301			else {    // !HTMLCanvasElement.prototype.toBlob
1302				// https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement.toBlob
1303				var binStr = atob(canvas.toDataURL("image/png").split(',')[1]),
1304					len = binStr.length,
1305					arr = new Uint8Array(len);
1306
1307				for (var i = 0; i < len; i++) {
1308					arr[i] = binStr.charCodeAt(i);
1309				}
1310
1311				saveBlob(new Blob([arr], {type: "image/png"}));
1312			}
1313		};
1314
1315		var restoreCanvasSize = function () {
1316			// restore canvas size
1317			if (old_size) app.setCanvasSize(old_size[0], old_size[1]);
1318			app.render();
1319		};
1320
1321		// background option
1322		if (!fill_background) app.renderer.setClearColor(0, 0);
1323
1324		// render
1325		app.renderer.preserveDrawingBuffer = true;
1326
1327		if (app.effect) {
1328			app.effect.render(app.scene, app.camera);
1329		}
1330		else {
1331			app.renderer.render(app.scene, app.camera);
1332		}
1333
1334		// restore clear color
1335		var bgcolor = conf.bgColor;
1336		app.renderer.setClearColor(bgcolor || 0, (bgcolor === null) ? 0 : 1);
1337
1338		if (fill_background && bgcolor === null) {
1339			var canvas = document.createElement("canvas");
1340			canvas.width = width;
1341			canvas.height = height;
1342
1343			var ctx = canvas.getContext("2d");
1344			if (fill_background && bgcolor === null) {
1345				// render "sky-like" background
1346				var grad = ctx.createLinearGradient(0, 0, 0, height);
1347				grad.addColorStop(0, "#98c8f6");
1348				grad.addColorStop(0.4, "#cbebff");
1349				grad.addColorStop(1, "#f0f9ff");
1350				ctx.fillStyle = grad;
1351				ctx.fillRect(0, 0, width, height);
1352			}
1353
1354			var image = new Image();
1355			image.onload = function () {
1356				// draw webgl canvas image
1357				ctx.drawImage(image, 0, 0, width, height);
1358
1359				// save canvas image
1360				saveCanvasImage(canvas);
1361				restoreCanvasSize();
1362			};
1363			image.src = app.renderer.domElement.toDataURL("image/png");
1364		}
1365		else {
1366			// save webgl canvas image
1367			saveCanvasImage(app.renderer.domElement);
1368			restoreCanvasSize();
1369		}
1370	};
1371
1372	(function () {
1373
1374		var path = [];
1375
1376		app.measure = {
1377
1378			isActive: false,
1379
1380			precision: 3,
1381
1382			start: function () {
1383				app.scene.remove(app.queryMarker);
1384
1385				if (!this.geom) {
1386					var opt = conf.measure.marker;
1387					this.geom = new THREE.SphereBufferGeometry(opt.radius, 32, 32);
1388					this.mtl = new THREE.MeshLambertMaterial({color: opt.color, opacity: opt.opacity, transparent: (opt.opacity < 1)});
1389					opt = conf.measure.line;
1390					this.lineMtl = new THREE.LineBasicMaterial({color: opt.color});
1391					this.markerGroup = new Q3DGroup();
1392					this.markerGroup.name = "measure marker";
1393					this.lineGroup = new Q3DGroup();
1394					this.lineGroup.name = "measure line";
1395				}
1396
1397				this.isActive = true;
1398
1399				app.scene.add(this.markerGroup);
1400				app.scene.add(this.lineGroup);
1401
1402				this.addPoint(app.queryTargetPosition);
1403			},
1404
1405			addPoint: function (pt) {
1406				// add a marker
1407				var marker = new THREE.Mesh(this.geom, this.mtl);
1408				marker.position.copy(pt);
1409				marker.onBeforeRender = app.queryMarker.onBeforeRender;
1410
1411				this.markerGroup.updateMatrixWorld();
1412				this.markerGroup.add(marker);
1413
1414				path.push(marker.position);
1415
1416				if (path.length > 1) {
1417					// add a line
1418					var v = path[path.length - 2].toArray().concat(path[path.length - 1].toArray()),
1419						geom = new THREE.BufferGeometry().setAttribute("position", new THREE.Float32BufferAttribute(v, 3)),
1420						line = new THREE.Line(geom, this.lineMtl);
1421					this.lineGroup.add(line);
1422				}
1423
1424				app.render();
1425				this.showResult();
1426			},
1427
1428			removeLastPoint: function () {
1429				path.pop();
1430				this.markerGroup.children.pop();
1431				this.lineGroup.children.pop();
1432
1433				app.render();
1434
1435				if (path.length) this.showResult();
1436				else app.cleanView();
1437			},
1438
1439			clear: function () {
1440				if (!this.isActive) return;
1441
1442				this.markerGroup.clear();
1443				this.lineGroup.clear();
1444
1445				app.scene.remove(this.markerGroup);
1446				app.scene.remove(this.lineGroup);
1447
1448				path = [];
1449				this.isActive = false;
1450			},
1451
1452			formatLength: function (length) {
1453				return (length) ? length.toFixed(this.precision) : 0;
1454			},
1455
1456			showResult: function () {
1457				var vec2 = new THREE.Vector2(),
1458					zScale = app.scene.userData.zScale;
1459				var total = 0, totalxy = 0, dz = 0;
1460				if (path.length > 1) {
1461					var dxy;
1462					for (var i = path.length - 1; i > 0; i--) {
1463						dxy = vec2.copy(path[i]).distanceTo(path[i - 1]);
1464						dz = (path[i].z - path[i - 1].z) / zScale;
1465
1466						total += Math.sqrt(dxy * dxy + dz * dz);
1467						totalxy += dxy;
1468					}
1469					dz = (path[path.length - 1].z - path[0].z) / zScale;
1470				}
1471
1472				var html = '<table class="measure">';
1473				html += "<tr><td>Total distance:</td><td>" + this.formatLength(total) + " m</td><td></td></tr>";
1474				html += "<tr><td>Horizontal distance:</td><td>" + this.formatLength(totalxy) + " m</td><td></td></tr>";
1475				html += "<tr><td>Vertical difference:</td><td>" + this.formatLength(dz) + ' m</td><td><span class="tooltip tooltip-btn" data-tooltip="elevation difference between start point and end point">?</span></td></tr>';
1476				html += "</table>";
1477
1478				gui.popup.show(html, "Measure distance");
1479			}
1480		};
1481	})();
1482
1483
1484	/*
1485	Q3D.gui
1486	*/
1487	var VIS = "visible";
1488
1489	function CE(tagName, parent, innerHTML) {
1490		var elem = document.createElement(tagName);
1491		if (parent) parent.appendChild(elem);
1492		if (innerHTML) elem.innerHTML = innerHTML;
1493		return elem;
1494	}
1495
1496	function ON_CLICK(id, listener) {
1497		var e = document.getElementById(id);
1498		if (e) e.addEventListener("click", listener);
1499	}
1500
1501	gui.modules = [];
1502
1503	gui.init = function () {
1504		// tool buttons
1505		ON_CLICK("layerbtn", function () {
1506			if (!gui.layerPanel.initialized) gui.layerPanel.init();
1507
1508			if (gui.layerPanel.isVisible()) {
1509				gui.layerPanel.hide();
1510			}
1511			else {
1512				if (gui.popup.isVisible()) {
1513					gui.popup.hide();
1514				}
1515				gui.layerPanel.show();
1516			}
1517		});
1518
1519		ON_CLICK("infobtn", function () {
1520			gui.layerPanel.hide();
1521
1522			if (gui.popup.isVisible() && gui.popup.content == "pageinfo") gui.popup.hide();
1523			else gui.showInfo();
1524		});
1525
1526		var btn = E("animbtn");
1527		if (conf.animation.enabled && btn) {
1528			var anim = app.animation.keyframes;
1529
1530			var playButton = function () {
1531				btn.className = "playbtn";
1532			};
1533
1534			var pauseButton = function () {
1535				btn.className = "pausebtn";
1536			};
1537
1538			btn.onclick = function () {
1539				if (anim.isActive) {
1540					anim.pause();
1541					playButton();
1542				}
1543				else if (anim.isPaused) {
1544					anim.resume();
1545					pauseButton();
1546				}
1547				else anim.start();
1548			};
1549
1550			app.addEventListener('animationStarted', pauseButton);
1551			app.addEventListener('animationStopped', playButton);
1552		}
1553
1554		// popup
1555		ON_CLICK("closebtn", app.cleanView);
1556		ON_CLICK("zoomtolayer", function () {
1557			app.cameraAction.zoomToLayer(app.selectedLayer);
1558		});
1559		ON_CLICK("zoomtopoint", function () {
1560			app.cameraAction.zoom();
1561		});
1562		ON_CLICK("orbitbtn", function () {
1563			app.cameraAction.orbit();
1564		});
1565		ON_CLICK("measurebtn", function () {
1566			app.measure.start();
1567		});
1568
1569		// narrative box
1570		ON_CLICK("nextbtn", function () {
1571			app.animation.keyframes.resume();
1572		});
1573
1574		// attribution
1575		if (typeof proj4 === "undefined") {
1576			var e = E("lib_proj4js");
1577			if (e) e.classList.add("hidden");
1578		}
1579
1580		// initialize modules
1581		for (var i = 0; i < gui.modules.length; i++) {
1582			gui.modules[i].init();
1583		}
1584	};
1585
1586	gui.clean = function () {
1587		gui.popup.hide();
1588		if (gui.layerPanel.initialized) gui.layerPanel.hide();
1589	};
1590
1591	gui.popup = {
1592
1593		modal: false,
1594
1595		content: null,
1596
1597		timerId: null,
1598
1599		isVisible: function () {
1600			return E("popup").classList.contains(VIS);
1601		},
1602
1603		// show box
1604		// obj: html, element or content id ("queryresult" or "pageinfo")
1605		// modal: boolean
1606		// duration: int [milliseconds]
1607		show: function (obj, title, modal, duration) {
1608
1609			if (modal) app.pause();
1610			else if (this.modal) app.resume();
1611
1612			this.content = obj;
1613			this.modal = Boolean(modal);
1614
1615			var e = E("layerpanel");
1616			if (e) e.classList.remove(VIS);
1617
1618			var content = E("popupcontent");
1619			[content, E("queryresult"), E("pageinfo")].forEach(function (e) {
1620				if (e) e.classList.remove(VIS);
1621			});
1622
1623			if (obj == "queryresult" || obj == "pageinfo") {
1624				E(obj).classList.add(VIS);
1625			}
1626			else {
1627				if (obj instanceof HTMLElement) {
1628					content.innerHTML = "";
1629					content.appendChild(obj);
1630				}
1631				else {
1632					content.innerHTML = obj;
1633				}
1634				content.classList.add(VIS);
1635			}
1636			E("popupbar").innerHTML = title || "";
1637			E("popup").classList.add(VIS);
1638
1639			if (this.timerId !== null) {
1640				clearTimeout(this.timerId);
1641				this.timerId = null;
1642			}
1643
1644			if (duration) {
1645				this.timerId = setTimeout(function () {
1646					gui.popup.hide();
1647				}, duration);
1648			}
1649		},
1650
1651		hide: function () {
1652			E("popup").classList.remove(VIS);
1653			if (this.timerId !== null) clearTimeout(this.timerId);
1654			this.timerId = null;
1655			this.content = null;
1656			if (this.modal) app.resume();
1657		}
1658
1659	};
1660
1661	gui.showInfo = function () {
1662		var e = E("urlbox");
1663		if (e) e.value = app.currentViewUrl();
1664		gui.popup.show("pageinfo");
1665	};
1666
1667	gui.showQueryResult = function (point, layer, obj, show_coords) {
1668		// layer name
1669		var e = E("qr_layername");
1670		if (layer && e) e.innerHTML = layer.properties.name;
1671
1672		// clicked coordinates
1673		e = E("qr_coords_table");
1674		if (e) {
1675			if (show_coords) {
1676				e.classList.remove("hidden");
1677
1678				var pt = app.scene.toMapCoordinates(point);
1679
1680				e = E("qr_coords");
1681
1682				if (conf.coord.latlon) {
1683					var lonLat = proj4(app.scene.userData.proj).inverse([pt.x, pt.y]);
1684					e.innerHTML = Q3D.Utils.convertToDMS(lonLat[1], lonLat[0]) + ", Elev. " + pt.z.toFixed(2);
1685				}
1686				else {
1687					e.innerHTML = [pt.x.toFixed(2), pt.y.toFixed(2), pt.z.toFixed(2)].join(", ");
1688				}
1689
1690				if (conf.debugMode) {
1691					var p = app.scene.userData,
1692						be = p.baseExtent;
1693					e.innerHTML += "<br>WLD: " + [point.x.toFixed(8), point.y.toFixed(8), point.z.toFixed(8)].join(", ");
1694					e.innerHTML += "<br><br>ORG: " + [p.origin.x.toFixed(8), p.origin.y.toFixed(8), p.origin.z.toFixed(8)].join(", ");
1695					e.innerHTML += "<br>BE CNTR: " + [be.cx.toFixed(8), be.cy.toFixed(8)].join(", ");
1696					e.innerHTML += "<br>BE SIZE: " + [be.width.toFixed(8), be.height.toFixed(8)].join(", ");
1697					e.innerHTML += "<br>ROT: " + be.rotation + "<br>Z SC: " + p.zScale;
1698				}
1699			}
1700			else {
1701				e.classList.add("hidden");
1702			}
1703		}
1704
1705		e = E("qr_attrs_table");
1706		if (e) {
1707			for (var i = e.children.length - 1; i >= 0; i--) {
1708				if (e.children[i].tagName.toUpperCase() == "TR") e.removeChild(e.children[i]);
1709			}
1710
1711			if (layer && layer.properties.propertyNames !== undefined) {
1712				var row;
1713				for (var i = 0, l = layer.properties.propertyNames.length; i < l; i++) {
1714					row = document.createElement("tr");
1715					row.innerHTML = "<td>" + layer.properties.propertyNames[i] + "</td>" +
1716									"<td>" + obj.userData.properties[i] + "</td>";
1717					e.appendChild(row);
1718				}
1719				e.classList.remove("hidden");
1720			}
1721			else {
1722				e.classList.add("hidden");
1723			}
1724		}
1725		gui.popup.show("queryresult");
1726	};
1727
1728	gui.showPrintDialog = function () {
1729
1730		var f = CE("form");
1731		f.className = "print";
1732
1733		var d1 = CE("div", f, "Image Size");
1734		d1.style.textDecoration = "underline";
1735
1736		var d2 = CE("div", f),
1737			l1 = CE("label", d2, "Width:"),
1738			width = CE("input", d2);
1739		d2.style.cssFloat = "left";
1740		l1.htmlFor = width.id = width.name = "printwidth";
1741		width.type = "text";
1742		width.value = app.width;
1743		CE("span", d2, "px,");
1744
1745		var d3 = CE("div", f),
1746			l2 = CE("label", d3, "Height:"),
1747			height = CE("input", d3);
1748		l2.htmlFor = height.id = height.name = "printheight";
1749		height.type = "text";
1750		height.value = app.height;
1751		CE("span", d3, "px");
1752
1753		var d4 = CE("div", f),
1754			ka = CE("input", d4);
1755		ka.type = "checkbox";
1756		ka.checked = true;
1757		CE("span", d4, "Keep Aspect Ratio");
1758
1759		var d5 = CE("div", f, "Option");
1760		d5.style.textDecoration = "underline";
1761
1762		var d6 = CE("div", f),
1763			bg = CE("input", d6);
1764		bg.type = "checkbox";
1765		bg.checked = true;
1766		CE("span", d6, "Fill Background");
1767
1768		var d7 = CE("div", f),
1769			ok = CE("span", d7, "OK"),
1770			cancel = CE("span", d7, "Cancel");
1771		d7.className = "buttonbox";
1772
1773		CE("input", f).type = "submit";
1774
1775		// event handlers
1776		// width and height boxes
1777		var aspect = app.width / app.height;
1778
1779		width.oninput = function () {
1780			if (ka.checked) height.value = Math.round(width.value / aspect);
1781		};
1782
1783		height.oninput = function () {
1784			if (ka.checked) width.value = Math.round(height.value * aspect);
1785		};
1786
1787		ok.onclick = function () {
1788			gui.popup.show("Rendering...");
1789			window.setTimeout(function () {
1790				app.saveCanvasImage(width.value, height.value, bg.checked);
1791			}, 10);
1792		};
1793
1794		cancel.onclick = app.cleanView;
1795
1796		// enter key pressed
1797		f.onsubmit = function () {
1798			ok.onclick();
1799			return false;
1800		};
1801
1802		gui.popup.show(f, "Save Image", true);   // modal
1803	};
1804
1805	gui.layerPanel = {
1806
1807		init: function () {
1808			var panel = E("layerpanel");
1809
1810			var p, item, e, slider, o, select, i;
1811			Object.keys(app.scene.mapLayers).forEach(function (layerId) {
1812
1813				var layer = app.scene.mapLayers[layerId];
1814				p = layer.properties;
1815				item = CE("div", panel);
1816				item.className = "layer";
1817
1818				// visible
1819				e = CE("div", item, "<input type='checkbox'" +  ((p.visible) ? " checked" : "") + ">" + p.name);
1820				e.querySelector("input[type=checkbox]").addEventListener("change", function () {
1821					layer.visible = this.checked;
1822				});
1823
1824				// opacity slider
1825				e = CE("div", item, "Opacity: <input type='range'><output></output>");
1826				slider = e.querySelector("input[type=range]");
1827
1828				var label = e.querySelector("output");
1829
1830				o = parseInt(layer.opacity * 100);
1831				slider.value = o;
1832				slider.addEventListener("input", function () {
1833					label.innerHTML = this.value + " %";
1834				});
1835				slider.addEventListener("change", function () {
1836					label.innerHTML = this.value + " %";
1837					layer.opacity = this.value / 100;
1838				});
1839				label.innerHTML = o + " %";
1840
1841				// material dropdown
1842				if (p.mtlNames && p.mtlNames.length > 1) {
1843					select = CE("select", CE("div", item, "Material: "));
1844					for (i = 0; i < p.mtlNames.length; i++) {
1845						CE("option", select, p.mtlNames[i]).setAttribute("value", i);
1846					}
1847					select.value = p.mtlIdx;
1848					select.addEventListener("change", function () {
1849						layer.setCurrentMaterial(this.value);
1850					});
1851				}
1852			});
1853			gui.layerPanel.initialized = true;
1854		},
1855
1856		isVisible: function () {
1857			return E("layerpanel").classList.contains(VIS);
1858		},
1859
1860		show: function () {
1861			E("layerpanel").classList.add(VIS);
1862		},
1863
1864		hide: function () {
1865			E("layerpanel").classList.remove(VIS);
1866		}
1867
1868	};
1869
1870})();
1871
1872
1873// Q3D classes
1874
1875class Q3DGroup extends THREE.Group {
1876
1877	add(object) {
1878		super.add(object);
1879		object.updateMatrixWorld();
1880	}
1881
1882	clear() {
1883		for (var i = this.children.length - 1; i >= 0; i--) {
1884			this.remove(this.children[i]);
1885		}
1886	}
1887
1888}
1889
1890
1891/*
1892Q3DScene
1893.userData
1894	- baseExtent(cx, cy, width, height, rotation): map base extent in map coordinates. center is (cx, cy).
1895	- origin: origin of 3D world in map coordinates
1896	- zScale: vertical scale factor
1897	- proj: (optional) proj string. used to display clicked position in long/lat.
1898*/
1899class Q3DScene extends THREE.Scene {
1900
1901	constructor() {
1902		super();
1903
1904		this.autoUpdate = false;
1905
1906		this.mapLayers = {};    // map layers contained in this scene. key is layerId.
1907
1908		this.lightGroup = new Q3DGroup();
1909		this.lightGroup.name = "light";
1910		this.add(this.lightGroup);
1911
1912		this.labelGroup = new Q3DGroup();
1913		this.labelGroup.name = "label";
1914		this.add(this.labelGroup);
1915
1916		this.labelConnectorGroup = new Q3DGroup();
1917		this.labelConnectorGroup.name = "label connector";
1918		this.add(this.labelConnectorGroup);
1919	}
1920
1921	add(object) {
1922		super.add(object);
1923		object.updateMatrixWorld();
1924	}
1925
1926	loadJSONObject(jsonObject) {
1927		if (jsonObject.type == "scene") {
1928			var p = jsonObject.properties;
1929			if (p !== undefined) {
1930				// fog
1931				if (p.fog) {
1932					this.fog = new THREE.FogExp2(p.fog.color, p.fog.density);
1933				}
1934
1935				// light
1936				var rotation0 = (this.userData.baseExtent) ? this.userData.baseExtent.rotation : 0;
1937				if (p.light != this.userData.light || p.baseExtent.rotation != rotation0) {
1938					this.lightGroup.clear();
1939					this.buildLights(Q3D.Config.lights[p.light] || Q3D.Config.lights.directional, p.baseExtent.rotation);
1940					this.dispatchEvent({type: "lightChanged", light: p.light});
1941				}
1942
1943				var be = p.baseExtent;
1944				p.pivot = new THREE.Vector3(be.cx, be.cy, p.origin.z).sub(p.origin);
1944   // 2D center of extent in 3D world coordinates
1945
1946				// set initial camera position and parameters
1947				if (this.userData.origin === undefined) {
1948
1949					var s = be.width,
1950						v = Q3D.Config.viewpoint,
1951						pos, focal;
1952
1953					if (v.pos === undefined) {
1954						v = v.default;
1955						if (be.rotation) {
1956							v = {
1957								pos: v.pos.clone().applyAxisAngle(Q3D.uv.k, be.rotation * Q3D.deg2rad),
1958								lookAt: v.lookAt.clone().applyAxisAngle(Q3D.uv.k, be.rotation * Q3D.deg2rad)
1959							};
1960						}
1961						pos = v.pos.clone().multiplyScalar(s).add(p.pivot);
1962						focal = v.lookAt.clone().multiplyScalar(s).add(p.pivot);
1963					}
1964					else {
1965						pos = new THREE.Vector3().copy(v.pos).sub(p.origin);
1966						focal = new THREE.Vector3().copy(v.lookAt).sub(p.origin);
1967					}
1968
1969					pos.z *= p.zScale;
1970					focal.z *= p.zScale;
1971
1972					var near = 0.001 * s,
1973						far = 100 * s;
1974
1975					this.requestCameraUpdate(pos, focal, near, far);
1976				}
1977
1978				if (p.baseExtent.rotation != rotation0) {
1979					this.dispatchEvent({type: "mapRotationChanged", rotation: p.baseExtent.rotation});
1980				}
1981
1982				this.userData = p;
1983			}
1984
1985			// load layers
1986			if (jsonObject.layers !== undefined) {
1987				jsonObject.layers.forEach(function (layer) {
1988					this.loadJSONObject(layer);
1989				}, this);
1990			}
1991		}
1992		else if (jsonObject.type == "layer") {
1993			var layer = this.mapLayers[jsonObject.id];
1994			if (layer === undefined) {
1995				// create a layer
1996				var type = jsonObject.properties.type;
1997				if (type == "dem") layer = new Q3DDEMLayer();
1998				else if (type == "point") layer = new Q3DPointLayer();
1999				else if (type == "line") layer = new Q3DLineLayer();
2000				else if (type == "polygon") layer = new Q3DPolygonLayer();
2001				else if (type == "pc") layer = new Q3DPointCloudLayer();
2002				else {
2003					console.error("unknown layer type:" + type);
2004					return;
2005				}
2006				layer.id = jsonObject.id;
2007				layer.objectGroup.userData.layerId = jsonObject.id;
2008				layer.addEventListener("renderRequest", this.requestRender.bind(this));
2009
2010				this.mapLayers[jsonObject.id] = layer;
2011				this.add(layer.objectGroup);
2012			}
2013
2014			layer.loadJSONObject(jsonObject, this);
2015
2016			this.requestRender();
2017		}
2018		else if (jsonObject.type == "block") {
2019			var layer = this.mapLayers[jsonObject.layer];
2020			if (layer === undefined) {
2021				// console.error("layer not exists:" + jsonObject.layer);
2022				return;
2023			}
2024			layer.loadJSONObject(jsonObject, this);
2025
2026			this.requestRender();
2027		}
2028	}
2029
2030	buildLights(lights, rotation) {
2031		var p, light;
2032		for (var i = 0; i < lights.length; i++) {
2033			p = lights[i];
2034			if (p.type == "ambient") {
2035				light = new THREE.AmbientLight(p.color, p.intensity);
2036			}
2037			else if (p.type == "directional") {
2038				light = new THREE.DirectionalLight(p.color, p.intensity);
2039				light.position.copy(Q3D.uv.j)
2040								.applyAxisAngle(Q3D.uv.i, p.altitude * Q3D.deg2rad)
2041								.applyAxisAngle(Q3D.uv.k, (rotation - p.azimuth) * Q3D.deg2rad);
2042			}
2043			else if (p.type == "point") {
2044				light = new THREE.PointLight(p.color, p.intensity);
2045				light.position.set(0, 0, p.height);
2046			}
2047			else {
2048				continue;
2049			}
2050			this.lightGroup.add(light);
2051		}
2052	}
2053
2054	requestRender() {
2055		this.dispatchEvent({type: "renderRequest"});
2056	}
2057
2058	requestCameraUpdate(pos, focal, near, far) {
2059		this.dispatchEvent({type: "cameraUpdateRequest", pos: pos, focal: focal, near: near, far: far});
2060	}
2061
2062	visibleObjects(labelVisible) {
2063		var layer, objs = [];
2064		for (var id in this.mapLayers) {
2065			layer = this.mapLayers[id];
2066			if (layer.visible) {
2067				objs = objs.concat(layer.visibleObjects());
2068				if (labelVisible && layer.labels) objs = objs.concat(layer.labels);
2069			}
2070		}
2071		return objs;
2072	}
2073
2074	// 3D world coordinates to map coordinates
2075	toMapCoordinates(pt) {
2076		var p = this.userData;
2077		return {
2078			x: p.origin.x + pt.x,
2079			y: p.origin.y + pt.y,
2080			z: p.origin.z + pt.z / p.zScale
2081		};
2082	}
2083
2084	// map coordinates to 3D world coordinates
2085	toWorldCoordinates(pt, isLonLat) {
2086		var p = this.userData;
2087		if (isLonLat && typeof proj4 !== "undefined") {
2088			// WGS84 long,lat to map coordinates
2089			var t = proj4(p.proj).forward([pt.x, pt.y]);
2090			pt = {x: t[0], y: t[1], z: pt.z};
2091		}
2092
2093		return {
2094			x: pt.x - p.origin.x,
2095			y: pt.y - p.origin.y,
2096			z: (pt.z - p.origin.z) * p.zScale
2097		};
2098	}
2099
2100	// return bounding box in 3d world coordinates
2101	boundingBox() {
2102		var box = new THREE.Box3();
2103		for (var id in this.mapLayers) {
2104			if (this.mapLayers[id].visible) {
2105				box.union(this.mapLayers[id].boundingBox());
2106			}
2107		}
2108		return box;
2109	}
2110
2111}
2112
2113
2114class Q3DMaterial {
2115
2116	constructor() {
2117		this.loaded = false;
2118	}
2119
2120	// material: a THREE.Material-based object
2121	set(material) {
2122		this.mtl = material;
2123		this.origProp = {};
2124		return this;
2125	}
2126
2127	// callback is called when material has been completely loaded
2128	loadJSONObject(jsonObject, callback) {
2129		this.origProp = jsonObject;
2130		this.groupId = jsonObject.mtlIndex;
2131
2132		var m = jsonObject, opt = {}, defer = false;
2133
2134		if (m.ds) opt.side = THREE.DoubleSide;
2135
2136		if (m.flat) opt.flatShading = true;
2137
2138		// texture
2139		if (m.image !== undefined) {
2140			var _this = this;
2141			if (m.image.url !== undefined) {
2142				opt.map = Q3D.application.loadTextureFile(m.image.url, function () {
2143					_this._loadCompleted(callback);
2144				});
2145				defer = true;
2146			}
2147			else if (m.image.object !== undefined) {    // WebKit Bridge
2148				opt.map = new THREE.Texture(m.image.object.toImageData());
2149				opt.map.needsUpdate = true;
2150
2151				delete m.image.object;
2152			}
2153			else {    // base64
2154				var img = new Image();
2155				img.onload = function () {
2156					opt.map.needsUpdate = true;
2157					_this._loadCompleted(callback);
2158				};
2159				img.src = m.image.base64;
2160				opt.map = new THREE.Texture(img);
2161				defer = true;
2162
2163				delete m.image.base64;
2164			}
2165			opt.map.anisotropy = Q3D.Config.texture.anisotropy;
2166		}
2167
2168		if (m.c !== undefined) opt.color = m.c;
2169
2170		if (m.o !== undefined && m.o < 1) {
2171			opt.opacity = m.o;
2172			opt.transparent = true;
2173		}
2174
2175		if (m.t) opt.transparent = true;
2176
2177		if (m.w) opt.wireframe = true;
2178
2179		if (m.bm) {
2180			this.mtl = new THREE.MeshBasicMaterial(opt);
2181		}
2182		else if (m.type == Q3D.MaterialType.MeshLambert) {
2183			this.mtl = new THREE.MeshLambertMaterial(opt);
2184		}
2185		else if (m.type == Q3D.MaterialType.MeshPhong) {
2186			this.mtl = new THREE.MeshPhongMaterial(opt);
2187		}
2188		else if (m.type == Q3D.MaterialType.MeshToon) {
2189			this.mtl = new THREE.MeshToonMaterial(opt);
2190		}
2191		else if (m.type == Q3D.MaterialType.Point) {
2192			opt.size = m.s;
2193			this.mtl = new THREE.PointsMaterial(opt);
2194		}
2195		else if (m.type == Q3D.MaterialType.Line) {
2196
2197			if (m.dashed) {
2198				opt.dashSize = Q3D.Config.line.dash.dashSize;
2199				opt.gapSize = Q3D.Config.line.dash.gapSize;
2200				this.mtl = new THREE.LineDashedMaterial(opt);
2201			}
2202			else {
2203				this.mtl = new THREE.LineBasicMaterial(opt);
2204			}
2205		}
2206		else if (m.type == Q3D.MaterialType.MeshLine) {
2207
2208			opt.lineWidth = m.thickness;
2209			if (m.dashed) {
2210				opt.dashArray = 0.03;
2211				opt.dashRatio = 0.45;
2212				opt.dashOffset = 0.015;
2213				opt.transparent = true;
2214			}
2215			// opt.sizeAttenuation = 1;
2216
2217			var mtl = this.mtl = new MeshLineMaterial(opt);
2218			var updateAspect = this._listener = function () {
2219				mtl.resolution = new THREE.Vector2(Q3D.application.width, Q3D.application.height);
2220			};
2221
2222			updateAspect();
2223			Q3D.application.addEventListener("canvasSizeChanged", updateAspect);
2224		}
2225		else if (m.type == Q3D.MaterialType.Sprite) {
2226			opt.color = 0xffffff;
2227			this.mtl = new THREE.SpriteMaterial(opt);
2228		}
2229		else {
2230			if (m.roughness !== undefined) opt.roughness = m.roughness;
2231			if (m.metalness !== undefined) opt.metalness = m.metalness;
2232
2233			this.mtl = new THREE.MeshStandardMaterial(opt);
2234		}
2235
2236		if (!defer) this._loadCompleted(callback);
2237	}
2238
2239	_loadCompleted(anotherCallback) {
2240		this.loaded = true;
2241
2242		if (this._callbacks !== undefined) {
2243			for (var i = 0; i < this._callbacks.length; i++) {
2244				this._callbacks[i]();
2245			}
2246			this._callbacks = [];
2247		}
2248
2249		if (anotherCallback) anotherCallback();
2250	}
2251
2252	callbackOnLoad(callback) {
2253		if (this.loaded) return callback();
2254
2255		if (this._callbacks === undefined) this._callbacks = [];
2256		this._callbacks.push(callback);
2257	}
2258
2259	dispose() {
2260		if (!this.mtl) return;
2261
2262		if (this.mtl.map) this.mtl.map.dispose();   // dispose of texture
2263		this.mtl.dispose();
2264		this.mtl = null;
2265
2266		if (this._listener) {
2267			Q3D.application.removeEventListener("canvasSizeChanged", this._listener);
2268			this._listener = undefined;
2269		}
2270	}
2271}
2272
2273
2274class Q3DMaterials extends THREE.EventDispatcher {
2275
2276	constructor() {
2277		super();
2278		this.array = [];
2279	}
2280
2281	// material: instance of Q3DMaterial object or THREE.Material-based object
2282	add(material) {
2283		if (material instanceof Q3DMaterial) {
2284			this.array.push(material);
2285		}
2286		else {
2287			this.array.push(new Q3DMaterial().set(material));
2288		}
2289	}
2290
2291	get(index) {
2292		return this.array[index];
2293	}
2294
2295	mtl(index) {
2296		return this.array[index].mtl;
2297	}
2298
2299	loadJSONObject(jsonObject) {
2300		var _this = this, iterated = false;
2301		var callback = function () {
2302			if (iterated) _this.dispatchEvent({type: "renderRequest"});
2303		};
2304
2305		for (var i = 0, l = jsonObject.length; i < l; i++) {
2306			var mtl = new Q3DMaterial();
2307			mtl.loadJSONObject(jsonObject[i], callback);
2308			this.add(mtl);
2309		}
2310		iterated = true;
2311	}
2312
2313	dispose() {
2314		for (var i = 0, l = this.array.length; i < l; i++) {
2315			this.array[i].dispose();
2316		}
2317		this.array = [];
2318	}
2319
2320	addFromObject3D(object) {
2321		var mtls = [];
2322
2323		object.traverse(function (obj) {
2324			if (obj.material === undefined) return;
2325			((obj.material instanceof Array) ? obj.material : [obj.material]).forEach(function (mtl) {
2326				if (mtls.indexOf(mtl) == -1) {
2327					mtls.push(mtl);
2328				}
2329			});
2330		});
2331
2332		for (var i = 0, l = mtls.length; i < l; i++) {
2333			this.array.push(new Q3DMaterial().set(mtls[i]));
2334		}
2335	}
2336
2337	// opacity
2338	opacity() {
2339		if (this.array.length == 0) return 1;
2340
2341		var sum = 0;
2342		for (var i = 0, l = this.array.length; i < l; i++) {
2343			sum += this.array[i].mtl.opacity;
2344		}
2345		return sum / this.array.length;
2346	}
2347
2348	setOpacity(opacity) {
2349		var m;
2350		for (var i = 0, l = this.array.length; i < l; i++) {
2351			m = this.array[i];
2352			m.mtl.transparent = Boolean(m.origProp.t) || (opacity < 1);
2353			m.mtl.opacity = opacity;
2354		}
2355	}
2356
2357	// wireframe: boolean
2358	setWireframeMode(wireframe) {
2359		var m;
2360		for (var i = 0, l = this.array.length; i < l; i++) {
2361			m = this.array[i];
2362			if (m.origProp.w || m.mtl instanceof THREE.LineBasicMaterial) continue;
2363			m.mtl.wireframe = wireframe;
2364		}
2365	}
2366
2367	removeItem(material, dispose) {
2368		for (var i = this.array.length - 1; i >= 0; i--) {
2369			if (this.array[i].mtl === material) {
2370				this.array.splice(i, 1);
2371				break;
2372			}
2373		}
2374		if (dispose) material.dispose();
2375	}
2376
2377	removeGroupItems(groupId) {
2378		for (var i = this.array.length - 1; i >= 0; i--) {
2379			if (this.array[i].groupId === groupId) {
2380				this.array.splice(i, 1);
2381			}
2382		}
2383	}
2384
2385}
2386
2387
2388class Q3DDEMBlockBase {
2389
2390	constructor() {
2391		this.materials = [];
2392		this.currentMtlIndex = 0;
2393	}
2394
2395	loadJSONObject(obj, layer, callback) {
2396		this.data = obj;
2397
2398		// load material
2399		var m, mtl;
2400		for (var i = 0, l = (obj.materials || []).length; i < l; i++) {
2401			m = obj.materials[i];
2402
2403			mtl = new Q3DMaterial();
2404			mtl.loadJSONObject(m, function () {
2405				layer.requestRender();
2406			});
2407			this.materials[m.mtlIndex] = mtl;
2408
2409			if (m.useNow) {
2410				this.currentMtlIndex = m.mtlIndex;
2411				if (this.obj) {
2412					layer.materials.removeItem(this.obj.material, true);
2413
2414					this.obj.material = mtl.mtl;
2415					layer.requestRender();
2416				}
2417				layer.materials.add(mtl);
2418			}
2419		}
2420	}
2421
2422}
2423
2424
2425class Q3DDEMBlock extends Q3DDEMBlockBase {
2426
2427	loadJSONObject(obj, layer, callback) {
2428		super.loadJSONObject(obj, layer, callback);
2429
2430		if (obj.grid === undefined) return;
2431
2432		// create a plane geometry
2433		var geom, grid = obj.grid;
2434		if (layer.geometryCache) {
2435			var params = layer.geometryCache.parameters || {};
2436			if (params.width === obj.width && params.height === obj.height &&
2437				params.widthSegments === grid.width - 1 && params.heightSegments === grid.height - 1) {
2438
2439				geom = layer.geometryCache.clone();
2440				geom.parameters = layer.geometryCache.parameters;
2441			}
2442		}
2443		geom = geom || new THREE.PlaneBufferGeometry(obj.width, obj.height, grid.width - 1, grid.height - 1);
2444		layer.geometryCache = geom;
2445
2446		// create a mesh
2447		var mesh = new THREE.Mesh(geom, (this.materials[this.currentMtlIndex] || {}).mtl);
2448		mesh.position.fromArray(obj.translate);
2449		mesh.scale.z = obj.zScale;
2450		layer.addObject(mesh);
2451
2452		// set z values
2453		var buildGeometry = function (grid_values) {
2454			var vertices = geom.attributes.position.array;
2455			for (var i = 0, j = 0, l = vertices.length; i < l; i++, j += 3) {
2456				vertices[j + 2] = grid_values[i];
2457			}
2458			geom.attributes.position.needsUpdate = true;
2459			geom.computeVertexNormals();
2460
2461			if (callback) callback(mesh);
2462		};
2463
2464		if (grid.url !== undefined) {
2465			Q3D.application.loadFile(grid.url, "arraybuffer", function (buf) {
2466				grid.array = new Float32Array(buf);
2467				buildGeometry(grid.array);
2468			});
2469		}
2470		else {
2471			if (grid.binary !== undefined) {
2472				// WebKit Bridge
2473				grid.array = new Float32Array(grid.binary.buffer, 0, grid.width * grid.height);
2474			}
2475			buildGeometry(grid.array);
2476		}
2477
2478		this.obj = mesh;
2479		return mesh;
2480	}
2481
2482	buildSides(layer, parent, material, z0) {
2483		var planeWidth = this.data.width,
2484			planeHeight = this.data.height,
2485			grid = this.data.grid,
2486			grid_values = grid.array,
2487			w = grid.width,
2488			h = grid.height,
2489			k = w * (h - 1);
2490
2491		var band_width = -2 * z0;
2492
2493		// front and back
2494		var geom_fr = new THREE.PlaneBufferGeometry(planeWidth, band_width, w - 1, 1),
2495			geom_ba = geom_fr.clone();
2496
2497		var vertices_fr = geom_fr.attributes.position.array,
2498			vertices_ba = geom_ba.attributes.position.array;
2499
2500		var i, mesh;
2501		for (i = 0; i < w; i++) {
2502			vertices_fr[i * 3 + 1] = grid_values[k + i];
2503			vertices_ba[i * 3 + 1] = grid_values[w - 1 - i];
2504		}
2505		mesh = new THREE.Mesh(geom_fr, material);
2506		mesh.rotation.x = Math.PI / 2;
2507		mesh.position.y = -planeHeight / 2;
2508		mesh.name = "side";
2509		parent.add(mesh);
2510
2511		mesh = new THREE.Mesh(geom_ba, material);
2512		mesh.rotation.x = Math.PI / 2;
2513		mesh.rotation.y = Math.PI;
2514		mesh.position.y = planeHeight / 2;
2515		mesh.name = "side";
2516		parent.add(mesh);
2517
2518		// left and right
2519		var geom_le = new THREE.PlaneBufferGeometry(band_width, planeHeight, 1, h - 1),
2520			geom_ri = geom_le.clone();
2521
2522		var vertices_le = geom_le.attributes.position.array,
2523			vertices_ri = geom_ri.attributes.position.array;
2524
2525		for (i = 0; i < h; i++) {
2526			vertices_le[(i * 2 + 1) * 3] = grid_values[w * i];
2527			vertices_ri[i * 2 * 3] = -grid_values[w * (i + 1) - 1];
2528		}
2529		mesh = new THREE.Mesh(geom_le, material);
2530		mesh.rotation.y = -Math.PI / 2;
2531		mesh.position.x = -planeWidth / 2;
2532		mesh.name = "side";
2533		parent.add(mesh);
2534
2535		mesh = new THREE.Mesh(geom_ri, material);
2536		mesh.rotation.y = Math.PI / 2;
2537		mesh.position.x = planeWidth / 2;
2538		mesh.name = "side";
2539		parent.add(mesh);
2540
2541		// bottom
2542		var geom = new THREE.PlaneBufferGeometry(planeWidth, planeHeight, 1, 1);
2543		mesh = new THREE.Mesh(geom, material);
2544		mesh.rotation.x = Math.PI;
2545		mesh.position.z = z0;
2546		mesh.name = "bottom";
2547		parent.add(mesh);
2548
2549		parent.updateMatrixWorld();
2550	}
2551
2552	addEdges(layer, parent, material, z0) {
2553
2554		var i, x, y,
2555			grid = this.data.grid,
2556			grid_values = grid.array,
2557			w = grid.width,
2558			h = grid.height,
2559			k = w * (h - 1),
2560			planeWidth = this.data.width,
2561			planeHeight = this.data.height,
2562			hpw = planeWidth / 2,
2563			hph = planeHeight / 2,
2564			psw = planeWidth / (w - 1),
2565			psh = planeHeight / (h - 1);
2566
2567		var vl = [];
2568
2569		// terrain edges
2570		var vl_fr = [],
2571			vl_bk = [],
2572			vl_le = [],
2573			vl_ri = [];
2574
2575		for (i = 0; i < w; i++) {
2576			x = -hpw + psw * i;
2577			vl_fr.push(x, -hph, grid_values[k + i]);
2578			vl_bk.push(x, hph, grid_values[i]);
2579		}
2580
2581		for (i = 0; i < h; i++) {
2582			y = hph - psh * i;
2583			vl_le.push(-hpw, y, grid_values[w * i]);
2584			vl_ri.push(hpw, y, grid_values[w * (i + 1) - 1]);
2585		}
2586
2587		vl.push(vl_fr, vl_bk, vl_le, vl_ri);
2588
2589		if (z0 !== undefined) {
2590			// horizontal rectangle at bottom
2591			vl.push([-hpw, -hph, z0,
2592						hpw, -hph, z0,
2593						hpw,  hph, z0,
2594					-hpw,  hph, z0,
2595					-hpw, -hph, z0]);
2596
2597			// vertical lines at corners
2598			[[-hpw, -hph, grid_values[grid_values.length - w]],
2599				[ hpw, -hph, grid_values[grid_values.length - 1]],
2600				[ hpw,  hph, grid_values[w - 1]],
2601				[-hpw,  hph, grid_values[0]]].forEach(function (v) {
2602
2603				vl.push([v[0], v[1], v[2], v[0], v[1], z0]);
2604
2605			});
2606		}
2607
2608		vl.forEach(function (v) {
2609
2610			var geom = new THREE.BufferGeometry().setAttribute("position", new THREE.Float32BufferAttribute(v, 3));
2611			var obj = new THREE.Line(geom, material);
2612			obj.name = "frame";
2613			parent.add(obj);
2614
2615		});
2616
2617		parent.updateMatrixWorld();
2618	}
2619
2620	// add quad wireframe
2621	addWireframe(layer, parent, material) {
2622
2623		var grid = this.data.grid,
2624			grid_values = grid.array,
2625			w = grid.width,
2626			h = grid.height,
2627			planeWidth = this.data.width,
2628			planeHeight = this.data.height,
2629			hpw = planeWidth / 2,
2630			hph = planeHeight / 2,
2631			psw = planeWidth / (w - 1),
2632			psh = planeHeight / (h - 1);
2633
2634		var v, geom, x, y, vx, vy, group = new THREE.Group();
2635
2636		for (x = w - 1; x >= 0; x--) {
2637			v = [];
2638			vx = -hpw + psw * x;
2639
2640			for (y = h - 1; y >= 0; y--) {
2641				v.push(vx, hph - psh * y, grid_values[x + w * y]);
2642			}
2643
2644			geom = new THREE.BufferGeometry().setAttribute("position", new THREE.Float32BufferAttribute(v, 3));
2645
2646			group.add(new THREE.Line(geom, material));
2647		}
2648
2649		for (y = h - 1; y >= 0; y--) {
2650			v = [];
2651			vy = hph - psh * y;
2652
2653			for (x = w - 1; x >= 0; x--) {
2654				v.push(-hpw + psw * x, vy, grid_values[x + w * y]);
2655			}
2656
2657			geom = new THREE.BufferGeometry().setAttribute("position", new THREE.Float32BufferAttribute(v, 3));
2658
2659			group.add(new THREE.Line(geom, material));
2660		}
2661
2662		parent.add(group);
2663		parent.updateMatrixWorld();
2664	}
2665
2666	getValue(x, y) {
2667		var grid = this.data.grid;
2668		if (0 <= x && x < grid.width && 0 <= y && y < grid.height) return grid.array[x + grid.width * y];
2669		return null;
2670	}
2671
2672	contains(x, y) {
2673		var translate = this.data.translate,
2674			xmin = translate[0] - this.data.width / 2,
2675			xmax = translate[0] + this.data.width / 2,
2676			ymin = translate[1] - this.data.height / 2,
2677			ymax = translate[1] + this.data.height / 2;
2678		if (xmin <= x && x <= xmax && ymin <= y && y <= ymax) return true;
2679		return false;
2680	}
2681}
2682
2683
2684class Q3DClippedDEMBlock extends Q3DDEMBlockBase {
2685
2686	loadJSONObject(obj, layer, callback) {
2687		super.loadJSONObject(obj, layer, callback);
2688
2689		if (obj.geom === undefined) return;
2690
2691		var geom = new THREE.BufferGeometry(),
2692			mesh = new THREE.Mesh(geom, (this.materials[this.currentMtlIndex] || {}).mtl);
2693		mesh.position.fromArray(obj.translate);
2694		mesh.scale.z = obj.zScale;
2695		layer.addObject(mesh);
2696
2697		var _this = this;
2698		var buildGeometry = function (obj) {
2699
2700			var v = obj.triangles.v,
2701				origin = layer.sceneData.origin,
2702				be = layer.sceneData.baseExtent,
2703				base_width = be.width,
2704				base_height = be.height,
2705				x0 = be.cx - origin.x - base_width * 0.5,
2706				y0 = be.cy - origin.y - base_height * 0.5;
2707
2708			var normals = [], uvs = [];
2709			for (var i = 0, l = v.length; i < l; i += 3) {
2710				normals.push(0, 0, 1);
2711				uvs.push((v[i] - x0) / base_width, (v[i + 1] - y0) / base_height);
2712			}
2713
2714			geom.setIndex(obj.triangles.f);
2715			geom.setAttribute("position", new THREE.Float32BufferAttribute(v, 3));
2716			geom.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
2717			geom.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
2718			geom.computeVertexNormals();
2719
2720			geom.attributes.position.needsUpdate = true;
2721			geom.attributes.normal.needsUpdate = true;
2722			geom.attributes.uv.needsUpdate = true;
2723
2724			_this.data.polygons = obj.polygons;
2725			if (callback) callback(mesh);
2726		};
2727
2728		if (obj.geom.url !== undefined) {
2729			Q3D.application.loadFile(obj.geom.url, "json", function (obj) {
2730				buildGeometry(obj);
2731			});
2732		}
2733		else {    // local mode or WebKit Bridge
2734			buildGeometry(obj.geom);
2735		}
2736
2737		this.obj = mesh;
2738		return mesh;
2739	}
2740
2741	buildSides(layer, parent, material, z0) {
2742		var polygons = this.data.polygons,
2743			bzFunc = function (x, y) { return z0; };
2744
2745		// make back-side material for bottom
2746		var mat_back = material.clone();
2747		mat_back.side = THREE.BackSide;
2748		layer.materials.add(mat_back);
2749
2750		var geom, mesh, shape;
2751		for (var i = 0, l = polygons.length; i < l; i++) {
2752			var bnds = polygons[i];
2753
2754			// sides
2755			for (var j = 0, m = bnds.length; j < m; j++) {
2756				geom = Q3D.Utils.createWallGeometry(bnds[j], bzFunc, true);
2757				mesh = new THREE.Mesh(geom, material);
2758				mesh.name = "side";
2759				parent.add(mesh);
2760			}
2761
2762			// bottom
2763			shape = new THREE.Shape(Q3D.Utils.flatArrayToVec2Array(bnds[0], 3));
2764			for (j = 1, m = bnds.length; j < m; j++) {
2765				shape.holes.push(new THREE.Path(Q3D.Utils.flatArrayToVec2Array(bnds[j], 3)));
2766			}
2767			geom = new THREE.ShapeBufferGeometry(shape);
2768			mesh = new THREE.Mesh(geom, mat_back);
2769			mesh.position.z = z0;
2770			mesh.name = "bottom";
2771			parent.add(mesh);
2772		}
2773		parent.updateMatrixWorld();
2774	}
2775
2776	// not implemented
2777	getValue(x, y) {
2778		return null;
2779	}
2780
2781	// not implemented
2782	contains(x, y) {
2783		return false;
2784	}
2785
2786}
2787
2788
2789class Q3DMapLayer extends THREE.EventDispatcher {
2790
2791	constructor() {
2792		super();
2793
2794		this.properties = {};
2795
2796		this.materials = new Q3DMaterials();
2797		this.materials.addEventListener("renderRequest", this.requestRender.bind(this));
2798
2799		this.objectGroup = new Q3DGroup();
2800		this.objectGroup.name = "layer";
2801		this.objects = [];
2802	}
2803
2804	addObject(object) {
2805		object.userData.layerId = this.id;
2806		this.objectGroup.add(object);
2807
2808		var o = this.objects;
2809		object.traverse(function (obj) {
2810			o.push(obj);
2811		});
2812		return this.objectGroup.children.length - 1;
2813	}
2814
2815	addObjects(objects) {
2816		for (var i = 0; i < objects.length; i++) {
2817			this.addObject(objects[i]);
2818		}
2819	}
2820
2821	clearObjects() {
2822		// dispose of geometries
2823		this.objectGroup.traverse(function (obj) {
2824			if (obj.geometry) obj.geometry.dispose();
2825		});
2826
2827		// dispose of materials
2828		this.materials.dispose();
2829
2830		// remove all child objects from object group
2831		for (var i = this.objectGroup.children.length - 1; i >= 0; i--) {
2832			this.objectGroup.remove(this.objectGroup.children[i]);
2833		}
2834		this.objects = [];
2835	}
2836
2837	visibleObjects() {
2838		return (this.visible) ? this.objects : [];
2839	}
2840
2841	loadJSONObject(jsonObject, scene) {
2842		if (jsonObject.type == "layer") {
2843			// properties
2844			if (jsonObject.properties !== undefined) {
2845				this.properties = jsonObject.properties;
2846				this.visible = (jsonObject.properties.visible || Q3D.Config.allVisible) ? true : false;
2847			}
2848
2849			if (jsonObject.data !== undefined) {
2850				this.clearObjects();
2851
2852				// materials
2853				if (jsonObject.data.materials !== undefined) {
2854					this.materials.loadJSONObject(jsonObject.data.materials);
2855				}
2856			}
2857
2858			this.sceneData = scene.userData;
2859		}
2860	}
2861
2862	get clickable() {
2863		return this.properties.clickable;
2864	}
2865
2866	get opacity() {
2867		return this.materials.opacity();
2868	}
2869
2870	set opacity(value) {
2871		this.materials.setOpacity(value);
2872
2873		if (this.labelGroup) {
2874			this.labelGroup.traverse(function (obj) {
2875				if (obj.material) obj.material.opacity = value;
2876			});
2877		}
2878
2879		if (this.labelConnectorGroup && this.labelConnectorGroup.children.length) {
2880			this.labelConnectorGroup.children[0].material.opacity = value;
2881		}
2882
2883		this.requestRender();
2884	}
2885
2886	get visible() {
2887		return this.objectGroup.visible;
2888	}
2889
2890	set visible(value) {
2891		this.objectGroup.visible = value;
2892		this.requestRender();
2893	}
2894
2895	boundingBox() {
2896		return new THREE.Box3().setFromObject(this.objectGroup);
2897	}
2898
2899	setWireframeMode(wireframe) {
2900		this.materials.setWireframeMode(wireframe);
2901	}
2902
2903	requestRender() {
2904		this.dispatchEvent({type: "renderRequest"});
2905	}
2906
2907}
2908
2909
2910class Q3DDEMLayer extends Q3DMapLayer {
2911
2912	constructor() {
2913		super();
2914		this.type = Q3D.LayerType.DEM;
2915		this.blocks = [];
2916	}
2917
2918	loadJSONObject(jsonObject, scene) {
2919		var old_blockIsClipped = this.properties.clipped;
2920
2921		super.loadJSONObject(jsonObject, scene);
2922		if (jsonObject.type == "layer") {
2923			if (old_blockIsClipped !== jsonObject.properties.clipped) {
2924				// DEM type changed
2925				this.blocks = [];
2926			}
2927
2928			var p = scene.userData,
2929				rotation = p.baseExtent.rotation;
2930
2931			if (jsonObject.properties.clipped) {
2932				this.objectGroup.position.set(0, 0, 0);
2933				this.objectGroup.rotation.z = 0;
2934
2935				if (rotation) {
2936					// rotate around center of base extent
2937					this.objectGroup.position.copy(p.pivot).negate();
2938					this.objectGroup.position.applyAxisAngle(Q3D.uv.k, rotation * Q3D.deg2rad);
2939					this.objectGroup.position.add(p.pivot);
2940					this.objectGroup.rotateOnAxis(Q3D.uv.k, rotation * Q3D.deg2rad);
2941				}
2942			}
2943			else {
2944				this.objectGroup.position.copy(p.pivot);
2945				this.objectGroup.position.z *= p.zScale;
2946				this.objectGroup.rotation.z = rotation * Q3D.deg2rad;
2947			}
2948			this.objectGroup.updateMatrixWorld();
2949
2950			if (jsonObject.data !== undefined) {
2951				jsonObject.data.forEach(function (obj) {
2952					this.buildBlock(obj, scene, this);
2953				}, this);
2954			}
2955		}
2956		else if (jsonObject.type == "block") {
2957			this.buildBlock(jsonObject, scene, this);
2958		}
2959	}
2960
2961	buildBlock(jsonObject, scene, layer) {
2962		var _this = this,
2963			block = this.blocks[jsonObject.block];
2964
2965		if (block === undefined) {
2966			block = (layer.properties.clipped) ? (new Q3DClippedDEMBlock()) : (ne
2966w Q3DDEMBlock());
2967			this.blocks[jsonObject.block] = block;
2968		}
2969
2970		block.loadJSONObject(jsonObject, this, function (mesh) {
2971
2972			var material;
2973			if (jsonObject.wireframe) {
2974				material = new Q3DMaterial();
2975				material.loadJSONObject(jsonObject.wireframe.mtl);
2976				_this.materials.add(material);
2977
2978				block.addWireframe(_this, mesh, material.mtl);
2979
2980				var mtl = block.material.mtl;
2981				mtl.polygonOffset = true;
2982				mtl.polygonOffsetFactor = 1;
2983				mtl.polygonOffsetUnits = 1;
2984			}
2985
2986			if (jsonObject.sides) {
2987				// sides and bottom
2988				material = new Q3DMaterial();
2989				material.loadJSONObject(jsonObject.sides.mtl);
2990				_this.materials.add(material);
2991
2992				block.buildSides(_this, mesh, material.mtl, jsonObject.sides.bottom);
2993				_this.sideVisible = true;
2994			}
2995
2996			if (jsonObject.edges) {
2997				material = new Q3DMaterial();
2998				material.loadJSONObject(jsonObject.edges.mtl);
2999				_this.materials.add(material);
3000
3001				block.addEdges(_this, mesh, material.mtl, (jsonObject.sides) ? jsonObject.sides.bottom : undefined);
3002			}
3003
3004			_this.requestRender();
3005		});
3006	}
3007
3008	// calculate elevation at the coordinates (x, y) on triangle face
3009	getZ(x, y) {
3010		for (var i = 0, l = this.blocks.length; i < l; i++) {
3011			var block = this.blocks[i],
3012				data = block.data;
3013
3014			if (!block.contains(x, y)) continue;
3015
3016			var ix = data.width / (data.grid.width - 1),
3017				iy = data.height / (data.grid.height - 1);
3018
3019			var xmin = data.translate[0] - data.width / 2,
3020				ymax = data.translate[1] + data.height / 2;
3021
3022			var mx0 = Math.floor((x - xmin) / ix),
3023				my0 = Math.floor((ymax - y) / iy);
3024
3025			var z = [block.getValue(mx0, my0),
3026						block.getValue(mx0 + 1, my0),
3027						block.getValue(mx0, my0 + 1),
3028						block.getValue(mx0 + 1, my0 + 1)];
3029
3030			var px0 = xmin + ix * mx0,
3031				py0 = ymax - iy * my0;
3032
3033			var sdx = (x - px0) / ix,
3034				sdy = (py0 - y) / iy;
3035
3036			if (sdx <= 1 - sdy) return z[0] + (z[1] - z[0]) * sdx + (z[2] - z[0]) * sdy;
3037			else return z[3] + (z[2] - z[3]) * (1 - sdx) + (z[1] - z[3]) * (1 - sdy);
3038		}
3039		return null;
3040	}
3041
3042	segmentizeLineString(lineString, zFunc) {
3043		// does not support multiple blocks
3044		if (zFunc === undefined) zFunc = function () { return 0; };
3045		var width = this.sceneData.width,
3046			height = this.sceneData.height;
3047		var xmin = -width / 2,
3048			ymax = height / 2;
3049		var grid = this.blocks[0].data.grid,
3050			ix = width / (grid.width - 1),
3051			iy = height / (grid.height - 1);
3052		var sort_func = function (a, b) { return a - b; };
3053
3054		var pts = [];
3055		for (var i = 1, l = lineString.length; i < l; i++) {
3056			var pt1 = lineString[i - 1], pt2 = lineString[i];
3057			var x1 = pt1[0], x2 = pt2[0], y1 = pt1[1], y2 = pt2[1], z1 = pt1[2], z2 = pt2[2];
3058			var nx1 = (x1 - xmin) / ix,
3059				nx2 = (x2 - xmin) / ix;
3060			var ny1 = (ymax - y1) / iy,
3061				ny2 = (ymax - y2) / iy;
3062			var ns1 = Math.abs(ny1 + nx1),
3063				ns2 = Math.abs(ny2 + nx2);
3064
3065			var p = [0], nvp = [[nx1, nx2], [ny1, ny2], [ns1, ns2]];
3066			for (var j = 0; j < 3; j++) {
3067				var v1 = nvp[j][0], v2 = nvp[j][1];
3068				if (v1 == v2) continue;
3069				var k = Math.ceil(Math.min(v1, v2));
3070				var n = Math.floor(Math.max(v1, v2));
3071				for (; k <= n; k++) {
3072					p.push((k - v1) / (v2 - v1));
3073				}
3074			}
3075
3076			p.sort(sort_func);
3077
3078			var x, y, z, lp = null;
3079			for (var j = 0, m = p.length; j < m; j++) {
3080				if (lp === p[j]) continue;
3081				if (p[j] == 1) break;
3082
3083				x = x1 + (x2 - x1) * p[j];
3084				y = y1 + (y2 - y1) * p[j];
3085
3086				if (z1 === undefined || z2 === undefined) z = zFunc(x, y);
3087				else z = z1 + (z2 - z1) * p[j];
3088
3089				pts.push(new THREE.Vector3(x, y, z));
3090
3091				// Q3D.Utils.putStick(x, y, zFunc);
3092
3093				lp = p[j];
3094			}
3095		}
3096		// last point (= the first point)
3097		var pt = lineString[lineString.length - 1];
3098		pts.push(new THREE.Vector3(pt[0], pt[1], (pt[2] === undefined) ? zFunc(pt[0], pt[1]) : pt[2]));
3099
3100		/*
3101		for (var i = 0, l = lineString.length - 1; i < l; i++) {
3102			Q3D.Utils.putStick(lineString[i][0], lineString[i][1], zFunc, 0.8);
3103		}
3104		*/
3105		return pts;
3106	}
3107
3108	setCurrentMaterial(mtlIndex) {
3109
3110		this.materials.removeGroupItems(this.currentMtlIndex);
3111
3112		this.currentMtlIndex = mtlIndex;
3113
3114		var b, m;
3115		for (var i = 0, l = this.blocks.length; i < l; i++) {
3116			b = this.blocks[i];
3117			m = b.materials[mtlIndex];
3118			if (m !== undefined) {
3119				b.obj.material = m.mtl;
3120				this.materials.add(m);
3121			}
3122		}
3123		this.requestRender();
3124	}
3125
3126	setSideVisible(visible) {
3127		this.sideVisible = visible;
3128		this.objectGroup.traverse(function (obj) {
3129			if (obj.name == "side" || obj.name == "bottom") obj.visible = visible;
3130		});
3131	}
3132
3133	// texture animation
3134	prepareTexAnimation(from, to) {
3135
3136		function imageData2Canvas(img) {
3137			var cnvs = document.createElement("canvas");
3138			cnvs.width = img.width;
3139			cnvs.height = img.height;
3140
3141			var ctx = cnvs.getContext("2d");
3142			ctx.putImageData(img, 0, 0);
3143			return cnvs;
3144		}
3145
3146		this.anim = [];
3147
3148		var m, canvas, ctx, opt, mtl;
3149		var img_from, img_to;
3150		for (var i = 0; i < this.blocks.length; i++) {
3151
3152			m = this.blocks[i].obj.material;
3153
3154			img_from = this.blocks[i].materials[from].mtl.map.image;
3155			img_to = this.blocks[i].materials[to].mtl.map.image;
3156
3157			canvas = document.createElement("canvas");
3158			canvas.width = (img_from.width > img_to.width) ? img_from.width : img_to.width;
3159			canvas.height = (img_from.width > img_to.width) ? img_from.height : img_to.height;
3160
3161			ctx = canvas.getContext("2d");
3162
3163			opt = {};
3164			opt.map = new THREE.CanvasTexture(canvas);
3165			opt.map.anisotropy = Q3D.Config.texture.anisotropy;
3166			opt.transparent = true;
3167
3168			mtl = undefined;
3169			if (m) {
3170				if (m.isMeshToonMaterial) {
3171					mtl = new THREE.MeshToonMaterial(opt);
3172				}
3173				else if (m.isMeshPhongMaterial) {
3174					mtl = new THREE.MeshPhongMaterial(opt);
3175				}
3176			}
3177			if (mtl === undefined) {
3178				mtl = new THREE.MeshLambertMaterial(opt);
3179			}
3180
3181			if (img_from instanceof ImageData) {    // WebKit Bridge
3182				img_from = imageData2Canvas(img_from);
3183				img_to = imageData2Canvas(img_to);
3184			}
3185
3186			this.blocks[i].obj.material = mtl;
3187
3188			this.materials.add(mtl);
3189
3190			this.anim.push({
3191				img_from: img_from,
3192				img_to: img_to,
3193				ctx: ctx,
3194				tex: mtl.map
3195			});
3196		}
3197	}
3198
3199	setTextureAt(progress, effect) {
3200
3201		if (this.anim === undefined) return;
3202
3203		var a, w, h, w0, h0, w1, h1, ew, ew1;
3204		for (var i = 0; i < this.anim.length; i++) {
3205			a = this.anim[i];
3206			w = a.ctx.canvas.width;
3207			h = a.ctx.canvas.height;
3208			w0 = a.img_from.width;
3209			h0 = a.img_from.height;
3210			w1 = a.img_to.width;
3211			h1 = a.img_to.height;
3212
3213			if (effect == 0) {  // fade in
3214				a.ctx.globalAlpha = 1;
3215				a.ctx.drawImage(a.img_from, 0, 0, w0, h0,
3216											0, 0, w, h);
3217				a.ctx.globalAlpha = progress;
3218				a.ctx.drawImage(a.img_to, 0, 0, w1, h1,
3219											0, 0, w, h);
3220			}
3221			else if (effect == 2) {  // slide to left (not used)
3222				if (progress === null) {
3223					a.ctx.drawImage(a.img_from, 0, 0, w0, h0,
3224												0, 0, w, h);
3225				}
3226				else {
3227					ew1 = w1 * progress;
3228					ew = w * progress;
3229					a.ctx.drawImage(a.img_to, w1 - ew1, 0, ew1, h1,
3230											w - ew, 0, ew, h);
3231				}
3232			}
3233			a.tex.needsUpdate = true;
3234		}
3235	}
3236}
3237
3238
3239class Q3DVectorLayer extends Q3DMapLayer {
3240
3241	constructor() {
3242		super();
3243		this.features = [];
3244		this.labels = [];
3245	}
3246
3247	// build(features, startIndex) {}
3248
3249	addFeature(featureIdx, f, objs) {
3250		super.addObjects(objs);
3251
3252		for (var i = 0; i < objs.length; i++) {
3253			objs[i].userData.featureIdx = featureIdx;
3254		}
3255		f.objs = objs;
3256
3257		this.features[featureIdx] = f;
3258		return f;
3259	}
3260
3261	clearLabels() {
3262		this.labels = [];
3263		if (this.labelGroup) this.labelGroup.clear();
3264		if (this.labelConnectorGroup) this.labelConnectorGroup.clear();
3265	}
3266
3267	buildLabels(features, getPointsFunc) {
3268		if (this.properties.label === undefined || getPointsFunc === undefined) return;
3269
3270		var _this = this,
3271			p = this.properties,
3272			label = p.label,
3273			bs = this.sceneData.baseExtent.width * 0.016,
3274			sc = bs * Math.pow(1.2, label.size);
3275
3276		var hasOtl = (label.olcolor !== undefined),
3277			hasConn = (label.cncolor !== undefined);
3278
3279		if (hasConn) {
3280			var line_mtl = new THREE.LineBasicMaterial({
3281				color: label.cncolor,
3282				opacity: this.materials.opacity(),
3283				transparent: true
3284			});
3285		}
3286
3287		var hasUnderline = Boolean(hasConn && label.underline);
3288		if (hasUnderline) {
3289			var ul_geom = new THREE.BufferGeometry();
3290			ul_geom.setAttribute("position", new THREE.Float32BufferAttribute([0, 0, 0, 1, 0, 0], 3));
3291
3292			var onBeforeRender = function (renderer, scene, camera, geometry, material, group) {
3293				this.quaternion.copy(camera.quaternion);
3294				this.updateMatrixWorld();
3295			};
3296		}
3297
3298		var canvas = document.createElement("canvas"),
3299			ctx = canvas.getContext("2d");
3300
3301		var font, tw, th, cw, ch;
3302		th = ch = Q3D.Config.label.canvasHeight;
3303		font = th + "px " + (label.font || "sans-serif");
3304
3305		canvas.height = ch;
3306
3307		var f, text, partIdx, vec, sprite, mtl, geom, conn, x, y, j, sc, opacity;
3308		var underline;
3309
3310		for (var i = 0, l = features.length; i < l; i++) {
3311			f = features[i];
3312			text = f.lbl;
3313			if (!text) continue;
3314
3315			opacity = this.materials.mtl(f.mtl).opacity;
3316
3317			partIdx = 0;
3318			getPointsFunc(f).forEach(function (pt) {
3319
3320				// label position
3321				vec = new THREE.Vector3(pt[0], pt[1], (label.relative) ? pt[2] + f.lh : f.lh);
3322
3323				// render label text
3324				ctx.font = font;
3325				tw = ctx.measureText(text).width + 2;
3326				cw = THREE.Math.ceilPowerOfTwo(tw);
3327				x = cw / 2;
3328				y = ch / 2;
3329
3330				canvas.width = cw;
3331				ctx.clearRect(0, 0, cw, ch);
3332
3333				if (label.bgcolor !== undefined) {
3334					ctx.fillStyle = label.bgcolor;
3335					ctx.roundRect((cw - tw) / 2, (ch - th) / 2, tw, th, 4).fill();    // definition is in this file
3336				}
3337
3338				ctx.font = font;
3339				ctx.textAlign = "center";
3340				ctx.textBaseline = "middle";
3341
3342				if (hasOtl) {
3343					// outline effect
3344					ctx.fillStyle = label.olcolor;
3345					for (j = 0; j < 9; j++) {
3346						if (j != 4) ctx.fillText(text, x + Math.floor(j / 3) - 1, y + j % 3 - 1);
3347					}
3348				}
3349
3350				ctx.fillStyle = label.color;
3351				ctx.fillText(text, x, y);
3352
3353				mtl = new THREE.SpriteMaterial({
3354					map: new THREE.TextureLoader().load(canvas.toDataURL(), function () { _this.requestRender(); }),
3355					opacity: opacity,
3356					transparent: true
3357				});
3358
3359				sprite = new THREE.Sprite(mtl);
3360				if (hasUnderline) {
3361					sprite.center.set((1 - tw / cw) * 0.5, 0);
3362				}
3363				else {
3364					sprite.center.set(0.5, 0);
3365				}
3366				sprite.position.copy(vec);
3367				sprite.scale.set(sc * cw / ch, sc, 1);
3368
3369				sprite.userData.layerId = this.id;
3370				sprite.userData.properties = f.prop;
3371				sprite.userData.objs = f.objs;
3372				sprite.userData.partIdx = partIdx;
3373				sprite.userData.isLabel = true;
3374
3375				this.labelGroup.add(sprite);
3376
3377				if (Q3D.Config.label.clickable) this.labels.push(sprite);
3378
3379				if (hasConn) {
3380					// a connector
3381					geom = new THREE.BufferGeometry();
3382					geom.setAttribute("position", new THREE.Float32BufferAttribute(vec.toArray().concat(pt), 3));
3383
3384					conn = new THREE.Line(geom, line_mtl);
3385					conn.userData = sprite.userData;
3386
3387					this.labelConnectorGroup.add(conn);
3388
3389					if (hasUnderline) {
3390						underline = new THREE.Line(ul_geom, line_mtl);
3391						underline.position.copy(vec);
3392						underline.scale.x = sc * tw / th;
3393						underline.updateMatrixWorld();
3394						underline.onBeforeRender = onBeforeRender;
3395						conn.add(underline);
3396					}
3397				}
3398				partIdx++;
3399			}, this);
3400		}
3401	}
3402
3403	loadJSONObject(jsonObject, scene) {
3404		super.loadJSONObject(jsonObject, scene);
3405		if (jsonObject.type == "layer") {
3406			if (jsonObject.data !== undefined) {
3407				this.features = [];
3408				this.clearLabels();
3409
3410				// build labels
3411				if (this.properties.label !== undefined) {
3412					// create a label group and a label connector group
3413					if (this.labelGroup === undefined) {
3414						this.labelGroup = new Q3DGroup();
3415						this.labelGroup.userData.layerId = this.id;
3416						this.labelGroup.visible = this.visible;
3417						scene.labelGroup.add(this.labelGroup);
3418					}
3419
3420					if (this.labelConnectorGroup === undefined) {
3421						this.labelConnectorGroup = new Q3DGroup();
3422						this.labelConnectorGroup.userData.layerId = this.id;
3423						this.labelConnectorGroup.visible = this.visible;
3424						scene.labelConnectorGroup.add(this.labelConnectorGroup);
3425					}
3426				}
3427
3428				(jsonObject.data.blocks || []).forEach(function (block) {
3429					if (block.url !== undefined) Q3D.application.loadJSONFile(block.url);
3430					else {
3431						this.build(block.features, block.startIndex);
3432						if (this.properties.label !== undefined) this.buildLabels(block.features);
3433					}
3434				}, this);
3435			}
3436		}
3437		else if (jsonObject.type == "block") {
3438			this.build(jsonObject.features, jsonObject.startIndex);
3439			if (this.properties.label !== undefined) this.buildLabels(jsonObject.features);
3440		}
3441	}
3442
3443	get visible() {
3444		return this.objectGroup.visible;
3445		// return super.visible;
3446	}
3447
3448	set visible(value) {
3449		if (this.labelGroup) this.labelGroup.visible = value;
3450		if (this.labelConnectorGroup) this.labelConnectorGroup.visible = value;
3451
3452		this.objectGroup.visible = value;
3453		this.requestRender();
3454		// super.visible = value;
3455	}
3456
3457}
3458
3459
3460class Q3DPointLayer extends Q3DVectorLayer {
3461
3462	constructor() {
3463		super();
3464		this.type = Q3D.LayerType.Point;
3465	}
3466
3467	loadJSONObject(jsonObject, scene) {
3468		if (jsonObject.type == "layer" && jsonObject.properties.objType == "3D Model" && jsonObject.data !== undefined) {
3469			if (this.models === undefined) {
3470				var _this = this;
3471
3472				this.models = new Q3DModels();
3473				this.models.addEventListener("modelLoaded", function (event) {
3474					_this.materials.addFromObject3D(event.model.scene);
3475					_this.requestRender();
3476				});
3477			}
3478			else {
3479				this.models.clear();
3480			}
3481			this.models.loadJSONObject(jsonObject.data.models);
3482		}
3483
3484		super.loadJSONObject(jsonObject, scene);
3485	}
3486
3487	build(features, startIndex) {
3488		var objType = this.properties.objType;
3489		if (objType == "Point") {
3490			return this.buildPoints(features, startIndex);
3491		}
3492		else if (objType == "Billboard") {
3493			return this.buildBillboards(features, startIndex);
3494		}
3495		else if (objType == "3D Model") {
3496			return this.buildModels(features, startIndex);
3497		}
3498
3499		var unitGeom, transform;
3500		if (this.cachedGeometryType === objType) {
3501			unitGeom = this.geometryCache;
3502			transform = this.transformCache;
3503		}
3504		else {
3505			var gt = this.geomAndTransformFunc(objType);
3506			unitGeom = gt[0];
3507			transform = gt[1];
3508		}
3509
3510		var f, mtl, pts, i, l, mesh, meshes;
3511		for (var fidx = 0; fidx < features.length; fidx++) {
3512			f = features[fidx];
3513			pts = f.geom.pts;
3514			mtl = this.materials.mtl(f.mtl);
3515
3516			meshes = [];
3517			for (i = 0, l = pts.length; i < l; i++) {
3518				mesh = new THREE.Mesh(unitGeom, mtl);
3519				transform(mesh, f.geom, pts[i]);
3520
3521				mesh.userData.properties = f.prop;
3522
3523				meshes.push(mesh);
3524			}
3525			this.addFeature(fidx + startIndex, f, meshes);
3526		}
3527
3528		this.cachedGeometryType = objType;
3529		this.geometryCache = unitGeom;
3530		this.transformCache = transform;
3531	}
3532
3533	geomAndTransformFunc(objType) {
3534
3535		var deg2rad = Q3D.deg2rad,
3536			rx = 90 * deg2rad;
3537
3538		if (objType == "Sphere") {
3539			return [
3540				new THREE.SphereBufferGeometry(1, 32, 32),
3541				function (mesh, geom, pt) {
3542					mesh.scale.setScalar(geom.r);
3543					mesh.position.fromArray(pt);
3544				}
3545			];
3546		}
3547		else if (objType == "Box") {
3548			return [
3549				new THREE.BoxBufferGeometry(1, 1, 1),
3550				function (mesh, geom, pt) {
3551					mesh.scale.set(geom.w, geom.h, geom.d);
3552					mesh.rotation.x = rx;
3553					mesh.position.set(pt[0], pt[1], pt[2] + geom.h / 2);
3554				}
3555			];
3556		}
3557		else if (objType == "Disk") {
3558			var sz = this.sceneData.zScale;
3559			return [
3560				new THREE.CircleBufferGeometry(1, 32),
3561				function (mesh, geom, pt) {
3562					mesh.scale.set(geom.r, geom.r * sz, 1);
3563					mesh.rotateOnWorldAxis(Q3D.uv.i, -geom.d * deg2rad);
3564					mesh.rotateOnWorldAxis(Q3D.uv.k, -geom.dd * deg2rad);
3565					mesh.position.fromArray(pt);
3566				}
3567			];
3568		}
3569		else if (objType == "Plane") {
3570			var sz = this.sceneData.zScale;
3571			return [
3572				new THREE.PlaneBufferGeometry(1, 1, 1, 1),
3573				function (mesh, geom, pt) {
3574					mesh.scale.set(geom.w, geom.l * sz, 1);
3575					mesh.rotateOnWorldAxis(Q3D.uv.i, -geom.d * deg2rad);
3576					mesh.rotateOnWorldAxis(Q3D.uv.k, -geom.dd * deg2rad);
3577					mesh.position.fromArray(pt);
3578				}
3579			];
3580		}
3581
3582		// Cylinder or Cone
3583		var radiusTop = (objType == "Cylinder") ? 1 : 0;
3584		return [
3585			new THREE.CylinderBufferGeometry(radiusTop, 1, 1, 32),
3586			function (mesh, geom, pt) {
3587				mesh.scale.set(geom.r, geom.h, geom.r);
3588				mesh.rotation.x = rx;
3589				mesh.position.set(pt[0], pt[1], pt[2] + geom.h / 2);
3590			}
3591		];
3592	}
3593
3594	buildPoints(features, startIndex) {
3595		var f, geom, mtl, obj;
3596		for (var fidx = 0; fidx < features.length; fidx++) {
3597			f = features[fidx];
3598
3599			geom = new THREE.BufferGeometry().setAttribute("position", new THREE.Float32BufferAttribute(f.geom.pts, 3));
3600			mtl = this.materials.mtl(f.mtl);
3601
3602			obj = new THREE.Points(geom, mtl);
3603			obj.userData.properties = f.prop;
3604
3605			this.addFeature(fidx + startIndex, f, [obj]);
3606		}
3607	}
3608
3609	buildBillboards(features, startIndex) {
3610
3611		features.forEach(function (f, fidx) {
3612
3613			var material = this.materials.get(f.mtl);
3614
3615			var sprite, sprites = [];
3616			for (var i = 0; i < f.geom.pts.length; i++) {
3617				sprite = new THREE.Sprite(material.mtl);
3618				sprite.position.fromArray(f.geom.pts[i]);
3619				sprite.userData.properties = f.prop;
3620
3621				sprites.push(sprite);
3622			}
3623
3624			material.callbackOnLoad(function () {
3625				var img = material.mtl.map.image;
3626				for (var i = 0; i < sprites.length; i++) {
3627					sprites[i].scale.set(f.geom.size,
3628										f.geom.size * img.height / img.width,
3629										1);
3630					sprites[i].updateMatrixWorld();
3631				}
3632			});
3633
3634			this.addFeature(fidx + startIndex, f, sprites);
3635		}, this);
3636	}
3637
3638	buildModels(features, startIndex) {
3639		var q = new THREE.Quaternion(),
3640			e = new THREE.Euler(),
3641			deg2rad = Q3D.deg2rad;
3642
3643		features.forEach(function (f, fidx) {
3644
3645			var model = this.models.get(f.model);
3646			if (!model) {
3647				console.log("3D Model: There is a missing model.");
3648				return;
3649			}
3650
3651			var parents = [];
3652			var pt, pts = f.geom.pts;
3653			for (var i = 0; i < pts.length; i++) {
3654				pt = pts[i];
3655
3656				var parent = new Q3DGroup();
3657				parent.position.fromArray(pt);
3658				parent.scale.set(1, 1, this.sceneData.zScale);
3659
3660				parent.userData.properties = f.prop;
3661
3662				parents.push(parent);
3663			}
3664
3665			model.callbackOnLoad(function (m) {
3666				var parent, obj;
3667				for (var i = 0; i < parents.length; i++) {
3668					parent = parents[i];
3669
3670					obj = m.scene.clone();
3671					obj.scale.setScalar(f.geom.scale);
3672
3673					if (obj.rotation.x) {
3674						// reset coordinate system to z-up and specified rotation
3675						obj.rotation.set(0, 0, 0);
3676						obj.quaternion.multiply(q.setFromEuler(e.set(f.geom.rotateX * deg2rad,
3677																		f.geom.rotateY * deg2rad,
3678																		f.geom.rotateZ * deg2rad,
3679																		f.geom.rotateO || "XYZ")));
3680					}
3681					else {
3682						// y-up to z-up and specified rotation
3683						obj.quaternion.multiply(q.setFromEuler(e.set(f.geom.rotateX * deg2rad,
3684																		f.geom.rotateY * deg2rad,
3685																		f.geom.rotateZ * deg2rad,
3686																		f.geom.rotateO || "XYZ")));
3687						obj.quaternion.multiply(q.setFromEuler(e.set(Math.PI / 2, 0, 0)));
3688					}
3689					parent.add(obj);
3690				}
3691			});
3692
3693			this.addFeature(fidx + startIndex, f, parents);
3694		}, this);
3695	}
3696
3697	buildLabels(features) {
3698		super.buildLabels(features, function (f) { return f.geom.pts; });
3699	}
3700
3701}
3702
3703
3704class Q3DLineLayer extends Q3DVectorLayer {
3705
3706	constructor() {
3707		super();
3708		this.type = Q3D.LayerType.Line;
3709	}
3710
3711	clearObjects() {
3712		super.clearObjects();
3713
3714		if (this.origMtls) {
3715			this.origMtls.dispose();
3716			this.origMtls = undefined;
3717		}
3718	}
3719
3720	build(features, startIndex) {
3721
3722		if (this._lastObjType !== this.properties.objType) this._createObject = null;
3723
3724		var createObject = this._createObject || this.createObjFunc(this.properties.objType);
3725
3726		var f, i, lines, obj, objs;
3727		for (var fidx = 0; fidx < features.length; fidx++) {
3728			f = features[fidx];
3729			lines = f.geom.lines;
3730
3731			objs = [];
3732			for (i = 0; i < lines.length; i++) {
3733				obj = createObject(f, lines[i]);
3734				obj.userData.properties = f.prop;
3735				obj.userData.mtl = f.mtl;
3736
3737				objs.push(obj);
3738			}
3739			this.addFeature(fidx + startIndex, f, objs);
3740		}
3741
3742		this._lastObjType = this.properties.objType;
3743		this._createObject = createObject;
3744	}
3745
3746	createObjFunc(objType) {
3747		var materials = this.materials;
3748
3749		if (objType == "Line") {
3750			return function (f, vertices) {
3751				var geom = new THREE.BufferGeometry().setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
3752
3753				var obj = new THREE.Line(geom, materials.mtl(f.mtl));
3754				if (obj.material instanceof THREE.LineDashedMaterial) obj.computeLineDistances();
3755				return obj;
3756			};
3757		}
3758		else if (objType == "Thick Line") {
3759			return function (f, vertices) {
3760				var line = new MeshLine();
3761				line.setPoints(vertices);
3762
3763				return new THREE.Mesh(line, materials.mtl(f.mtl));
3764			};
3765		}
3766		else if (objType == "Pipe" || objType == "Cone") {
3767			var jointGeom, cylinGeom;
3768			if (objType == "Pipe") {
3769				jointGeom = new THREE.SphereBufferGeometry(1, 32, 32);
3770				cylinGeom = new THREE.CylinderBufferGeometry(1, 1, 1, 32);
3771			}
3772			else {
3773				cylinGeom = new THREE.CylinderBufferGeometry(0, 1, 1, 32);
3774			}
3775
3776			var group, mesh, axis = Q3D.uv.j;
3777			var pt0 = new THREE.Vector3(), pt1 = new THREE.Vector3(), sub = new THREE.Vector3();
3778
3779			return function (f, points) {
3780				group = new Q3DGroup();
3781
3782				pt0.fromArray(points[0]);
3783				for (var i = 1, l = points.length; i < l; i++) {
3784					pt1.fromArray(points[i]);
3785
3786					mesh = new THREE.Mesh(cylinGeom, materials.mtl(f.mtl));
3787					mesh.scale.set(f.geom.r, pt0.distanceTo(pt1), f.geom.r);
3788					mesh.position.set((pt0.x + pt1.x) / 2, (pt0.y + pt1.y) / 2, (pt0.z + pt1.z) / 2);
3789					mesh.quaternion.setFromUnitVectors(axis, sub.subVectors(pt1, pt0).normalize());
3790					group.add(mesh);
3791
3792					if (jointGeom && i < l - 1) {
3793						mesh = new THREE.Mesh(jointGeom, materials.mtl(f.mtl));
3794						mesh.scale.setScalar(f.geom.r);
3795						mesh.position.copy(pt1);
3796						group.add(mesh);
3797					}
3798
3799					pt0.copy(pt1);
3800				}
3801				return group;
3802			};
3803		}
3804		else if (objType == "Box") {
3805			// In this method, box corners are exposed near joint when both azimuth and slope of
3806			// the segments of both sides are different. Also, some unnecessary faces are created.
3807			var faces = [], vi;
3808			vi = [[0, 5, 4], [4, 5, 1],   // left turn - top, side, bottom
3809				[3, 0, 7], [7, 0, 4],
3810				[6, 3, 2], [2, 3, 7],
3811				[4, 1, 0], [0, 1, 5],   // right turn - top, side, bottom
3812				[1, 2, 5], [5, 2, 6],
3813				[2, 7, 6], [6, 7, 3]];
3814
3815			for (var j = 0; j < 12; j++) {
3816				faces.push(new THREE.Face3(vi[j][0], vi[j][1], vi[j][2]));
3817			}
3818
3819			return function (f, points) {
3820				var geometry = new THREE.Geometry();
3821
3822				var geom, dist, rx, rz, wh4, vb4, vf4;
3823				var pt0 = new THREE.Vector3(), pt1 = new THREE.Vector3(), sub = new THREE.Vector3(),
3824					pt = new THREE.Vector3(), ptM = new THREE.Vector3(), scale1 = new THREE.Vector3(1, 1, 1),
3825					matrix = new THREE.Matrix4(), quat = new THREE.Quaternion();
3826
3827				pt0.fromArray(points[0]);
3828				for (var i = 1, l = points.length; i < l; i++) {
3829					pt1.fromArray(points[i]);
3830					dist = pt0.distanceTo(pt1);
3831					sub.subVectors(pt1, pt0);
3832					rx = Math.atan2(sub.z, Math.sqrt(sub.x * sub.x + sub.y * sub.y));
3833					rz = Math.atan2(sub.y, sub.x) - Math.PI / 2;
3834					ptM.set((pt0.x + pt1.x) / 2, (pt0.y + pt1.y) / 2, (pt0.z + pt1.z) / 2);   // midpoint
3835					quat.setFromEuler(new THREE.Euler(rx, 0, rz, "ZXY"));
3836					matrix.compose(ptM, quat, scale1);
3837
3838					// place a box to the segment
3839					geom = new THREE.BoxGeometry(f.geom.w, dist, f.geom.h);
3840					geom.applyMatrix(matrix);
3841					geometry.merge(geom);
3842
3843					// joint
3844					// 4 vertices of backward side of current segment
3845					wh4 = [[-f.geom.w / 2, f.geom.h / 2],
3846							[f.geom.w / 2, f.geom.h / 2],
3847							[f.geom.w / 2, -f.geom.h / 2],
3848							[-f.geom.w / 2, -f.geom.h / 2]];
3849					vb4 = [];
3850					for (j = 0; j < 4; j++) {
3851						pt.set(wh4[j][0], -dist / 2, wh4[j][1]);
3852						pt.applyMatrix4(matrix);
3853						vb4.push(pt.clone());
3854					}
3855
3856					if (vf4) {
3857						geom = new THREE.Geometry();
3858						geom.vertices = vf4.concat(vb4);
3859						geom.faces = faces;
3860						geometry.merge(geom);
3861					}
3862
3863					// 4 vertices of forward side
3864					vf4 = [];
3865					for (j = 0; j < 4; j++) {
3866						pt.set(wh4[j][0], dist / 2, wh4[j][1]);
3867						pt.applyMatrix4(matrix);
3868						vf4.push(new THREE.Vector3(pt.x, pt.y, pt.z));
3869					}
3870
3871					pt0.copy(pt1);
3872				}
3873
3874				geometry.faceVertexUvs = [[]];
3875				geometry.mergeVertices();
3876				geometry.computeFaceNormals();
3877				return new THREE.Mesh(geometry, materials.mtl(f.mtl));
3878			};
3879		}
3880		else if (objType == "Wall") {
3881			return function (f, vertices) {
3882				var bzFunc = function (x, y) { return f.geom.bh; };
3883				return new THREE.Mesh(Q3D.Utils.createWallGeometry(vertices, bzFunc),
3884									materials.mtl(f.mtl));
3885			};
3886		}
3887	}
3888
3889	buildLabels(features) {
3890		// Line layer doesn't support label
3891		// super.buildLabels(features);
3892	}
3893
3894	// prepare for growing line animation
3895	prepareAnimation(sequential) {
3896
3897		if (this.origMtls !== undefined) return;
3898
3899		function computeLineDistances(obj) {
3900			if (!obj.material.isLineDashedMaterial) return;
3901
3902			obj.computeLineDistances();
3903
3904			var dists = obj.geometry.attributes.lineDistance.array;
3905			obj.lineLength = dists[dists.length - 1];
3906
3907			for (var i = 0; i < dists.length; i++) {
3908				dists[i] /= obj.lineLength;
3909			}
3910		}
3911
3912		this.origMtls = new Q3DMaterials();
3913		this.origMtls.array = this.materials.array;
3914
3915		this.materials.array = [];
3916
3917		if (sequential) {
3918			var f, m, mtl, j;
3919			for (var i = 0; i < this.features.length; i++) {
3920				f = this.features[i];
3921				m = f.objs[0].material;
3922
3923				if (m.isMeshLineMaterial) {
3924					mtl = new MeshLineMaterial();
3925					mtl.color = m.color;
3926					mtl.opacity = m.opacity;
3927					mtl.lineWidth = m.lineWidth;
3928					mtl.dashArray = 2;
3929					mtl.transparent = true;
3930				}
3931				else {
3932					if (m.isLineDashedMaterial) {
3933						mtl = m.clone();
3934					}
3935					else {
3936						mtl = new THREE.LineDashedMaterial({color: m.color, opacity: m.opacity});
3937					}
3938					mtl.gapSize = 1;
3939				}
3940
3941				for (j = 0; j < f.objs.length; j++) {
3942					f.objs[j].material = mtl;
3943					computeLineDistances(f.objs[j]);
3944				}
3945				this.materials.add(mtl);
3946			}
3947		}
3948		else {
3949			var mtl, mtls = this.origMtls.array;
3950
3951			for (var i = 0; i < mtls.length; i++) {
3952				mtl = mtls[i].mtl;
3953
3954				if (mtl.isLineDashedMaterial) {
3955					mtl.gapSize = 1;
3956				}
3957				else if (mtl.isMeshLineMaterial) {
3958					mtl.dashArray = 2;
3959					mtl.transparent = true;
3960				}
3961				else if (mtl.isLineBasicMaterial) {
3962					mtl = new THREE.LineDashedMaterial({color: mtl.color, opacity: mtl.opacity});
3963				}
3964
3965				this.materials.add(mtl);
3966			}
3967
3968			var _this = this;
3969			this.objectGroup.traverse(function (obj) {
3970
3971				if (obj.userData.mtl !== undefined) {
3972					obj.material = _this.materials.mtl(obj.userData.mtl);
3973					computeLineDistances(obj);
3974				}
3975
3976			});
3977		}
3978	}
3979
3980	// length: number [0 - 1]
3981	setLineLength(length, featureIdx) {
3982
3983		if (this.origMtls === undefined) return;
3984
3985		var mtl;
3986		if (featureIdx === undefined) {
3987			var mtls = this.materials.array;
3988			for (var i = 0; i < mtls.length; i++) {
3989				mtl = mtls[i].mtl;
3990				if (mtl.isLineDashedMaterial) {
3991					mtl.dashSize = length;
3992				}
3993				else if (mtl.isMeshLineMaterial) {
3994					mtl.uniforms.dashOffset.value = -length;
3995				}
3996			}
3997		}
3998		else {
3999			mtl = this.features[featureIdx].objs[0].material;
4000			if (mtl.isLineDashedMaterial) {
4001				mtl.dashSize = length;
4002			}
4003			else if (mtl.isMeshLineMaterial) {
4004				mtl.uniforms.dashOffset.value = -length;
4005			}
4006		}
4007	}
4008
4009}
4010
4011
4012class Q3DPolygonLayer extends Q3DVectorLayer {
4013
4014	constructor() {
4015		super();
4016
4017		this.type = Q3D.LayerType.Polygon;
4018
4019		// for overlay
4020		this.borderVisible = true;
4021		this.sideVisible = true;
4022	}
4023
4024	build(features, startIndex) {
4025
4026		if (this.properties.objType !== this._lastObjType) this._createObject = null;
4027
4028		var createObject = this._createObject || this.createObjFunc(this.properties.objType);
4029
4030		var f, obj;
4031		for (var fidx = 0; fidx < features.length; fidx++) {
4032			f = features[fidx];
4033			obj = createObject(f);
4034			obj.userData.properties = f.prop;
4035
4036			this.addFeature(fidx + startIndex, f, [obj]);
4037		}
4038
4039		this._lastObjType = this.properties.objType;
4040		this._createObject = createObject;
4041	}
4042
4043	createObjFunc(objType) {
4044
4045		var materials = this.materials;
4046
4047		if (objType == "Polygon") {
4048			return function (f) {
4049				var geom = new THREE.BufferGeometry();
4050				geom.setAttribute("position", new THREE.Float32BufferAttribute(f.geom.triangles.v, 3));
4051				geom.setIndex(f.geom.triangles.f);
4052				geom = new THREE.Geometry().fromBufferGeometry(geom); // Flat shading doesn't work with combination of
4053																	// BufferGeometry and Lambert/Toon material.
4054				return new THREE.Mesh(geom, materials.mtl(f.mtl));
4055			};
4056		}
4057		else if (objType == "Extruded") {
4058			var createSubObject = function (f, polygon, z) {
4059				var i, l, j, m;
4060
4061				var shape = new THREE.Shape(Q3D.Utils.arrayToVec2Array(polygon[0]));
4062				for (i = 1, l = polygon.length; i < l; i++) {
4063					shape.holes.push(new THREE.Path(Q3D.Utils.arrayToVec2Array(polygon[i])));
4064				}
4065
4066				// extruded geometry
4067				var geom = new THREE.ExtrudeBufferGeometry(shape, {bevelEnabled: false, depth: f.geom.h});
4068				var mesh = new THREE.Mesh(geom, materials.mtl(f.mtl.face));
4069				mesh.position.z = z;
4070
4071				if (f.mtl.edge !== undefined) {
4072					// edge
4073					var edge, bnd, v,
4074						h = f.geom.h,
4075						mtl = materials.mtl(f.mtl.edge);
4076
4077					for (i = 0, l = polygon.length; i < l; i++) {
4078						bnd = polygon[i];
4079
4080						v = [];
4081						for (j = 0, m = bnd.length; j < m; j++) {
4082							v.push(bnd[j][0], bnd[j][1], 0);
4083						}
4084
4085						geom = new THREE.BufferGeometry().setAttribute("position", new THREE.Float32BufferAttribute(v, 3));
4086
4087						edge = new THREE.Line(geom, mtl);
4088						mesh.add(edge);
4089
4090						edge = new THREE.Line(geom, mtl);
4091						edge.position.z = h;
4092						mesh.add(edge);
4093
4094						// vertical lines
4095						for (j = 0, m = bnd.length - 1; j < m; j++) {
4096							v = [bnd[j][0], bnd[j][1], 0,
4097									bnd[j][0], bnd[j][1], h];
4098
4099							geom = new THREE.BufferGeometry().setAttribute("position", new THREE.Float32BufferAttribute(v, 3));
4100
4101							edge = new THREE.Line(geom, mtl);
4102							mesh.add(edge);
4103						}
4104					}
4105				}
4106				return mesh;
4107			};
4108
4109			var polygons, centroids;
4110
4111			return function (f) {
4112				polygons = f.geom.polygons;
4113				centroids = f.geom.centroids;
4114
4115				if (polygons.length == 1) return createSubObject(f, polygons[0], centroids[0][2]);
4116
4117				var group = new THREE.Group();
4118				for (var i = 0, l = polygons.length; i < l; i++) {
4119					group.add(createSubObject(f, polygons[i], centroids[i][2]));
4120				}
4121				return group;
4122			};
4123		}
4124		else if (objType == "Overlay") {
4125
4126			var _this = this;
4127
4128			return function (f) {
4129
4130				var geom = new THREE.BufferGeometry();
4131				geom.setIndex(f.geom.triangles.f);
4132				geom.setAttribute("position", new THREE.Float32BufferAttribute(f.geom.triangles.v, 3));
4133				geom.computeVertexNormals();
4134
4135				var mesh = new THREE.Mesh(geom, materials.mtl(f.mtl.face));
4136
4137				var rotation = _this.sceneData.baseExtent.rotation;
4138				if (rotation) {
4139					// rotate around center of base extent
4140					mesh.position.copy(_this.sceneData.pivot).negate();
4141					mesh.position.applyAxisAngle(Q3D.uv.k, rotation * Q3D.deg2rad);
4142					mesh.position.add(_this.sceneData.pivot);
4143					mesh.rotateOnAxis(Q3D.uv.k, rotation * Q3D.deg2rad);
4144				}
4145
4146				// borders
4147				if (f.geom.brdr !== undefined) {
4148					var bnds, i, l, j, m;
4149					for (i = 0, l = f.geom.brdr.length; i < l; i++) {
4150						bnds = f.geom.brdr[i];
4151						for (j = 0, m = bnds.length; j < m; j++) {
4152							geom = new THREE.BufferGeometry().setAttribute("position", new THREE.Float32BufferAttribute(bnds[j], 3));
4153
4154							mesh.add(new THREE.Line(geom, materials.mtl(f.mtl.brdr)));
4155						}
4156					}
4157				}
4158				return mesh;
4159			};
4160		}
4161	}
4162
4163	buildLabels(features) {
4164		super.buildLabels(features, function (f) { return f.geom.centroids; });
4165	}
4166
4167	setBorderVisible(visible) {
4168		if (this.properties.objType != "Overlay") return;
4169
4170		this.objectGroup.children.forEach(function (parent) {
4171			for (var i = 0, l = parent.children.length; i < l; i++) {
4172				var obj = parent.children[i];
4173				if (obj instanceof THREE.Line) obj.visible = visible;
4174			}
4175		});
4176		this.borderVisible = visible;
4177	}
4178
4179	setSideVisible(visible) {
4180		if (this.properties.objType != "Overlay") return;
4181
4182		this.objectGroup.children.forEach(function (parent) {
4183			for (var i = 0, l = parent.children.length; i < l; i++) {
4184				var obj = parent.children[i];
4185				if (obj instanceof THREE.Mesh) obj.visible = visible;
4186			}
4187		});
4188		this.sideVisible = visible;
4189	}
4190
4191}
4192
4193
4194class Q3DModel {
4195
4196	constructor() {
4197		this.loaded = false;
4198	}
4199
4200	// callback is called when model has been completely loaded
4201	load(url, callback) {
4202		var _this = this;
4203		Q3D.application.loadModelFile(url, function (model) {
4204			_this.model = model;
4205			_this._loadCompleted(callback);
4206		});
4207	}
4208
4209	loadData(data, ext, resourcePath, callback) {
4210		var _this = this;
4211		Q3D.application.loadModelData(data, ext, resourcePath, function (model) {
4212			_this.model = model;
4213			_this._loadCompleted(callback);
4214		});
4215	}
4216
4217	loadJSONObject(jsonObject, callback) {
4218		if (jsonObject.url !== undefined) {
4219			this.load(jsonObject.url, callback);
4220		}
4221		else {
4222			var b = atob(jsonObject.base64),
4223				len = b.length,
4224				bytes = new Uint8Array(len);
4225
4226			for (var i = 0; i < len; i++) {
4227				bytes[i] = b.charCodeAt(i);
4228			}
4229
4230			this.loadData(bytes.buffer, jsonObject.ext, jsonObject.resourcePath, callback);
4231		}
4232	}
4233
4234	_loadCompleted(anotherCallback) {
4235		this.loaded = true;
4236
4237		if (this._callbacks !== undefined) {
4238			for (var i = 0; i < this._callbacks.length; i++) {
4239				this._callbacks[i](this.model);
4240			}
4241			this._callbacks = [];
4242		}
4243
4244		if (anotherCallback) anotherCallback(this.model);
4245	}
4246
4247	callbackOnLoad(callback) {
4248		if (this.loaded) return callback(this.model);
4249
4250		if (this._callbacks === undefined) this._callbacks = [];
4251		this._callbacks.push(callback);
4252	}
4253
4254}
4255
4256
4257class Q3DModels extends THREE.EventDispatcher {
4258
4259	constructor() {
4260		super();
4261
4262		this.models = [];
4263		this.cache = {};
4264	}
4265
4266	loadJSONObject(jsonObject) {
4267		var _this = this;
4268		var callback = function (model) {
4269			_this.dispatchEvent({type: "modelLoaded", model: model});
4270		};
4271
4272		var model, url;
4273		for (var i = 0, l = jsonObject.length; i < l; i++) {
4274
4275			url = jsonObject[i].url;
4276			if (url !== undefined && this.cache[url] !== undefined) {
4277				model = this.cache[url];
4278			}
4279			else {
4280				model = new Q3DModel();
4281				model.loadJSONObject(jsonObject[i], callback);
4282
4283				if (url !== undefined) this.cache[url] = model;
4284			}
4285			this.models.push(model);
4286		}
4287	}
4288
4289	get(index) {
4290		return this.models[index];
4291	}
4292
4293	clear() {
4294		this.models = [];
4295	}
4296
4297}
4298
4299Q3D.Group = Q3DGroup;
4300Q3D.Scene = Q3DScene;
4301Q3D.Material = Q3DMaterial;
4302Q3D.Materials = Q3DMaterials;
4303Q3D.DEMBlock = Q3DDEMBlock;
4304Q3D.ClippedDEMBlock = Q3DClippedDEMBlock;
4305Q3D.MapLayer = Q3DMapLayer;
4306Q3D.DEMLayer = Q3DDEMLayer;
4307Q3D.VectorLayer = Q3DVectorLayer;
4308Q3D.PointLayer = Q3DPointLayer;
4309Q3D.LineLayer = Q3DLineLayer;
4310Q3D.PolygonLayer = Q3DPolygonLayer;
4311Q3D.Model = Q3DModel;
4312Q3D.Models = Q3DModels;
4313
4314
4315// Q3D.Utils - Utilities
4316Q3D.Utils = {};
4317
4318// Put a stick to given position (for debug)
4319Q3D.Utils.putStick = function (x, y, zFunc, h) {
4320	if (Q3D.Utils._stick_mat === undefined) Q3D.Utils._stick_mat = new THREE.LineBasicMaterial({color: 0xff0000});
4321	if (h === undefined) h = 0.2;
4322	if (zFunc === undefined) {
4323		zFunc = function (x, y) { return Q3D.application.scene.mapLayers[0].getZ(x, y); };
4324	}
4325	var z = zFunc(x, y);
4326	var geom = new THREE.Geometry();
4327	geom.vertices.push(new THREE.Vector3(x, y, z + h), new THREE.Vector3(x, y, z));
4328	var stick = new THREE.Line(geom, Q3D.Utils._stick_mat);
4329	Q3D.application.scene.add(stick);
4330};
4331
4332// convert latitude and longitude in degrees to the following format
4333// Ndd°mm′ss.ss″, Eddd°mm′ss.ss″
4334Q3D.Utils.convertToDMS = function (lat, lon) {
4335	function toDMS(degrees) {
4336		var deg = Math.floor(degrees),
4337			m = (degrees - deg) * 60,
4338			min = Math.floor(m),
4339			sec = (m - min) * 60;
4340		return deg + "°" + ("0" + min).slice(-2) + "′" + ((sec < 10) ? "0" : "") + sec.toFixed(2) + "″";
4341	}
4342
4343	return ((lat < 0) ? "S" : "N") + toDMS(Math.abs(lat)) + ", " +
4344		   ((lon < 0) ? "W" : "E") + toDMS(Math.abs(lon));
4345};
4346
4347Q3D.Utils.createWallGeometry = function (vert, bzFunc, buffer_geom) {
4348	var geom = new THREE.Geometry();
4349	for (var i = 0, l = vert.length; i < l; i += 3) {
4350		geom.vertices.push(
4351			new THREE.Vector3(vert[i], vert[i + 1], vert[i + 2]),
4352			new THREE.Vector3(vert[i], vert[i + 1], bzFunc(vert[i], vert[i + 1]))
4353		);
4354	}
4355
4356	for (var i = 1, i2 = 1, l = vert.length / 3; i < l; i++, i2 += 2) {
4357		geom.faces.push(
4358			new THREE.Face3(i2 - 1, i2, i2 + 1),
4359			new THREE.Face3(i2 + 1, i2, i2 + 2)
4360		);
4361	}
4362
4363	geom.computeFaceNormals();
4364
4365	if (buffer_geom) {
4366		return new THREE.BufferGeometry().fromGeometry(geom);
4367	}
4368	return geom;
4369};
4370
4371Q3D.Utils.arrayToVec2Array = function (points) {
4372	var pt, pts = [];
4373	for (var i = 0, l = points.length; i < l; i++) {
4374		pt = points[i];
4375		pts.push(new THREE.Vector2(pt[0], pt[1]));
4376	}
4377	return pts;
4378};
4379
4380Q3D.Utils.flatArrayToVec2Array = function (vertices, itemSize) {
4381	itemSize = itemSize || 2;
4382	var pts = [];
4383	for (var i = 0, l = vertices.length; i < l; i += itemSize) {
4384		pts.push(new THREE.Vector2(vertices[i], vertices[i + 1]));
4385	}
4386	return pts;
4387};
4388
4389Q3D.Utils.setGeometryUVs = function (geom, base_width, base_height) {
4390	var face, v, uvs = [];
4391	for (var i = 0, l = geom.vertices.length; i < l; i++) {
4392		v = geom.vertices[i];
4393		uvs.push(new THREE.Vector2(v.x / base_width + 0.5, v.y / base_height + 0.5));
4394	}
4395
4396	geom.faceVertexUvs[0] = [];
4397	for (var i = 0, l = geom.faces.length; i < l; i++) {
4398		face = geom.faces[i];
4399		geom.faceVertexUvs[0].push([uvs[face.a], uvs[face.b], uvs[face.c]]);
4400	}
4401};
4402
4403
4404// Q3D.Tweens
4405Q3D.Tweens = {};
4406
4407Q3D.Tweens.cameraMotion = {
4408
4409	type: Q3D.KeyframeType.CameraMotion,
4410
4411	curveFactor: 0,
4412
4413	init: function (group) {
4414
4415		var app = Q3D.application,
4416			zScale = app.scene.userData.zScale,
4417			keyframes = group.keyframes,
4418			prop_list = [];
4419
4420		var c = this.curveFactor, p, p0, phi, theta, dist, dist_list = [];
4421		var vec3 = new THREE.Vector3(),
4422			o = app.scene.userData.origin;
4423
4424		for (var i = 0; i < keyframes.length; i++) {
4425			p = keyframes[i].camera;
4426			vec3.set(p.x - p.fx, p.y - p.fy, (p.z - p.fz) * zScale);
4427			dist = vec3.length();
4428			theta = Math.acos(vec3.z / dist);
4429			phi = Math.atan2(vec3.y, vec3.x);
4430			p.phi = phi;
4431			prop_list.push({p: i, fx: p.fx - o.x, fy: p.fy - o.y, fz: (p.fz - o.z) * zScale, d: dist, theta: theta});  // map to 3D world
4432
4433			if (i > 0) {
4434				dist_list.push(Math.sqrt((p.x - p0.x) * (p.x - p0.x) + (p.y - p0.y) * (p.y - p0.y)));
4435			}
4436			p0 = p;
4437		}
4438		group.prop_list = prop_list;
4439
4440		var phi0, phi1, dz;
4441		group.onUpdate = function (obj, elapsed, is_first) {
4442
4443			p = obj.p - group.currentIndex;
4444			phi0 = keyframes[group.currentIndex].camera.phi;
4445			phi1 = (is_first) ? phi0 : keyframes[group.currentIndex + 1].camera.phi;
4446
4447			if (Math.abs(phi1 - phi0) > Math.PI) {  // take the shortest orbiting path
4448				phi1 += Math.PI * ((phi1 > phi0) ? -2 : 2);
4449			}
4450
4451			phi = phi0 * (1 - p) + phi1 * p;
4452
4453			vec3.set(Math.cos(phi) * Math.sin(obj.theta),
4454					 Math.sin(phi) * Math.sin(obj.theta),
4455					 Math.cos(obj.theta)).setLength(obj.d);
4456
4457			dz = (c) ? (1 - Math.pow(2 * p - 1, 2)) * dist_list[group.currentIndex] * c : 0;
4458
4459			app.camera.position.set(obj.fx + vec3.x, obj.fy + vec3.y, obj.fz + vec3.z + dz);
4460			app.camera.lookAt(obj.fx, obj.fy, obj.fz);
4461			app.controls.target.set(obj.fx, obj.fy, obj.fz);
4462		};
4463
4464		// initial position
4465		group.onUpdate(group.prop_list[0], 1, true);
4466	}
4467
4468};
4469
4470Q3D.Tweens.opacity = {
4471
4472	type: Q3D.KeyframeType.Opacity,
4473
4474	init: function (group, layer) {
4475
4476		var keyframes = group.keyframes;
4477
4478		for (var i = 0; i < keyframes.length; i++) {
4479			group.prop_list.push({opacity: keyframes[i].opacity});
4480		}
4481
4482		group.onUpdate = function (obj, elapsed) {
4483			layer.opacity = obj.opacity;
4484		};
4485
4486		// initial opacity
4487		group.onUpdate(group.prop_list[0]);
4488	}
4489
4490};
4491
4492Q3D.Tweens.texture = {
4493
4494	type: Q3D.KeyframeType.Texture,
4495
4496	init: function (group, layer) {
4497
4498		var keyframes = group.keyframes;
4499
4500		var idx_from, from, to, effect;
4501
4502		group.onStart = function () {
4503			idx_from = group.currentIndex;
4504			effect = keyframes[idx_from].effect;
4505			from = keyframes[idx_from].mtlIndex;
4506			to = keyframes[idx_from + 1].mtlIndex;
4507
4508			layer.prepareTexAnimation(from, to);
4509			layer.setTextureAt(null, effect);
4510		};
4511
4512		group.onUpdate = function (obj, elapsed) {
4513			layer.setTextureAt(obj.p - group.currentIndex, effect);
4514		};
4515
4516		for (var i = 0; i < keyframes.length; i++) {
4517			group.prop_list.push({p: i});
4518		}
4519	}
4520};
4521
4522Q3D.Tweens.lineGrowing = {
4523
4524	type: Q3D.KeyframeType.GrowingLine,
4525
4526	init: function (group, layer) {
4527		if (group._keyframes === undefined) group._keyframes = group.keyframes;
4528
4529		var effectItem = group._keyframes[0];
4530		if (effectItem.sequential) {
4531			group.keyframes = [];
4532
4533			var item;
4534			for (var i = 0; i < layer.features.length; i++) {
4535				item = layer.features[i].anim;
4536				item.easing = effectItem.easing;
4537				group.keyframes.push(item);
4538				group.prop_list.push({p: i});
4539			}
4540			group.keyframes.push({});
4541			group.prop_list.push({p: i});
4542
4543			group.onUpdate = function (obj, elapsed) {
4544				layer.setLineLength(obj.p - group.currentIndex, group.currentIndex);
4545			};
4546		}
4547		else {
4548			group.keyframes = [effectItem, {}];
4549			group.prop_list = [{p: 0}, {p: 1}];
4550
4551			group.onUpdate = function (obj, elapsed) {
4552				layer.setLineLength(obj.p);
4553			};
4554		}
4555
4556		layer.prepareAnimation(effectItem.sequential);
4557		layer.setLineLength(0);
4558	}
4559
4560};
4561
4562
4563// https://stackoverflow.com/a/7838871
4564CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
4565	if (w < 2 * r) r = w / 2;
4566	if (h < 2 * r) r = h / 2;
4567	this.beginPath();
4568	this.moveTo(x + r, y);
4569	this.arcTo(x + w, y, x + w, y + h, r);
4570	this.arcTo(x + w, y + h, x, y + h, r);
4571	this.arcTo(x, y + h, x, y, r);
4572	this.arcTo(x, y, x + w, y, r);
4573	this.closePath();
4574	return this;
4575};

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.