diff --git a/docs/examples/24/dat-gui/dat.gui.min.js b/docs/examples/24/dat-gui/dat.gui.min.js new file mode 100644 index 00000000..3543875a --- /dev/null +++ b/docs/examples/24/dat-gui/dat.gui.min.js @@ -0,0 +1,98 @@ +/** + * dat-gui JavaScript Controller Library + * http://code.google.com/p/dat-gui + * + * Copyright 2011 Data Arts Team, Google Creative Lab + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Modification for Qgis2threejs: + * - Fix for local files with IE (Line 61, Column 123). + */ + +var dat=dat||{};dat.gui=dat.gui||{};dat.utils=dat.utils||{};dat.controllers=dat.controllers||{};dat.dom=dat.dom||{};dat.color=dat.color||{};dat.utils.css=function(){return{load:function(e,a){a=a||document;var b=a.createElement("link");b.type="text/css";b.rel="stylesheet";b.href=e;a.getElementsByTagName("head")[0].appendChild(b)},inject:function(e,a){a=a||document;var b=document.createElement("style");b.type="text/css";b.innerHTML=e;a.getElementsByTagName("head")[0].appendChild(b)}}}(); +dat.utils.common=function(){var e=Array.prototype.forEach,a=Array.prototype.slice;return{BREAK:{},extend:function(b){this.each(a.call(arguments,1),function(a){for(var f in a)this.isUndefined(a[f])||(b[f]=a[f])},this);return b},defaults:function(b){this.each(a.call(arguments,1),function(a){for(var f in a)this.isUndefined(b[f])&&(b[f]=a[f])},this);return b},compose:function(){var b=a.call(arguments);return function(){for(var d=a.call(arguments),f=b.length-1;0<=f;f--)d=[b[f].apply(this,d)];return d[0]}}, +each:function(a,d,f){if(e&&a.forEach===e)a.forEach(d,f);else if(a.length===a.length+0)for(var c=0,p=a.length;c
this.__max&&(a=this.__max);void 0!==this.__step&&0!=a%this.__step&&(a=Math.round(a/this.__step)*this.__step);return b.superclass.prototype.setValue.call(this,a)},min:function(a){this.__min=a;return this},max:function(a){this.__max=a;return this},step:function(a){this.__step=a;return this}});return b}(dat.controllers.Controller,dat.utils.common);
+dat.controllers.NumberControllerBox=function(e,a,b){var d=function(f,c,e){function k(){var a=parseFloat(n.__input.value);b.isNaN(a)||n.setValue(a)}function l(a){var c=r-a.clientY;n.setValue(n.getValue()+c*n.__impliedStep);r=a.clientY}function q(){a.unbind(window,"mousemove",l);a.unbind(window,"mouseup",q)}this.__truncationSuspended=!1;d.superclass.call(this,f,c,e);var n=this,r;this.__input=document.createElement("input");this.__input.setAttribute("type","text");a.bind(this.__input,"change",k);a.bind(this.__input,
+"blur",function(){k();n.__onFinishChange&&n.__onFinishChange.call(n,n.getValue())});a.bind(this.__input,"mousedown",function(c){a.bind(window,"mousemove",l);a.bind(window,"mouseup",q);r=c.clientY});a.bind(this.__input,"keydown",function(a){13===a.keyCode&&(n.__truncationSuspended=!0,this.blur(),n.__truncationSuspended=!1)});this.updateDisplay();this.domElement.appendChild(this.__input)};d.superclass=e;b.extend(d.prototype,e.prototype,{updateDisplay:function(){var a=this.__input,c;if(this.__truncationSuspended)c=
+this.getValue();else{c=this.getValue();var b=Math.pow(10,this.__precision);c=Math.round(c*b)/b}a.value=c;return d.superclass.prototype.updateDisplay.call(this)}});return d}(dat.controllers.NumberController,dat.dom.dom,dat.utils.common);
+dat.controllers.NumberControllerSlider=function(e,a,b,d,f){function c(a,c,d,b,f){return b+(a-c)/(d-c)*(f-b)}var p=function(d,b,f,e,r){function y(d){d.preventDefault();var b=a.getOffset(h.__background),f=a.getWidth(h.__background);h.setValue(c(d.clientX,b.left,b.left+f,h.__min,h.__max));return!1}function g(){a.unbind(window,"mousemove",y);a.unbind(window,"mouseup",g);h.__onFinishChange&&h.__onFinishChange.call(h,h.getValue())}p.superclass.call(this,d,b,{min:f,max:e,step:r});var h=this;this.__background=
+document.createElement("div");this.__foreground=document.createElement("div");a.bind(this.__background,"mousedown",function(c){a.bind(window,"mousemove",y);a.bind(window,"mouseup",g);y(c)});a.addClass(this.__background,"slider");a.addClass(this.__foreground,"slider-fg");this.updateDisplay();this.__background.appendChild(this.__foreground);this.domElement.appendChild(this.__background)};p.superclass=e;p.useDefaultStyles=function(){b.inject(f)};d.extend(p.prototype,e.prototype,{updateDisplay:function(){var a=
+(this.getValue()-this.__min)/(this.__max-this.__min);this.__foreground.style.width=100*a+"%";return p.superclass.prototype.updateDisplay.call(this)}});return p}(dat.controllers.NumberController,dat.dom.dom,dat.utils.css,dat.utils.common,"/**\n * dat-gui JavaScript Controller Library\n * http://code.google.com/p/dat-gui\n *\n * Copyright 2011 Data Arts Team, Google Creative Lab\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n */\n\n.slider {\n box-shadow: inset 0 2px 4px rgba(0,0,0,0.15);\n height: 1em;\n border-radius: 1em;\n background-color: #eee;\n padding: 0 0.5em;\n overflow: hidden;\n}\n\n.slider-fg {\n padding: 1px 0 2px 0;\n background-color: #aaa;\n height: 1em;\n margin-left: -0.5em;\n padding-right: 0.5em;\n border-radius: 1em 0 0 1em;\n}\n\n.slider-fg:after {\n display: inline-block;\n border-radius: 1em;\n background-color: #fff;\n border: 1px solid #aaa;\n content: '';\n float: right;\n margin-right: -1em;\n margin-top: -1px;\n height: 0.9em;\n width: 0.9em;\n}");
+dat.controllers.FunctionController=function(e,a,b){var d=function(b,c,e){d.superclass.call(this,b,c);var k=this;this.__button=document.createElement("div");this.__button.innerHTML=void 0===e?"Fire":e;a.bind(this.__button,"click",function(a){a.preventDefault();k.fire();return!1});a.addClass(this.__button,"button");this.domElement.appendChild(this.__button)};d.superclass=e;b.extend(d.prototype,e.prototype,{fire:function(){this.__onChange&&this.__onChange.call(this);this.__onFinishChange&&this.__onFinishChange.call(this,
+this.getValue());this.getValue().call(this.object)}});return d}(dat.controllers.Controller,dat.dom.dom,dat.utils.common);
+dat.controllers.BooleanController=function(e,a,b){var d=function(b,c){d.superclass.call(this,b,c);var e=this;this.__prev=this.getValue();this.__checkbox=document.createElement("input");this.__checkbox.setAttribute("type","checkbox");a.bind(this.__checkbox,"change",function(){e.setValue(!e.__prev)},!1);this.domElement.appendChild(this.__checkbox);this.updateDisplay()};d.superclass=e;b.extend(d.prototype,e.prototype,{setValue:function(a){a=d.superclass.prototype.setValue.call(this,a);this.__onFinishChange&&
+this.__onFinishChange.call(this,this.getValue());this.__prev=this.getValue();return a},updateDisplay:function(){!0===this.getValue()?(this.__checkbox.setAttribute("checked","checked"),this.__checkbox.checked=!0):this.__checkbox.checked=!1;return d.superclass.prototype.updateDisplay.call(this)}});return d}(dat.controllers.Controller,dat.dom.dom,dat.utils.common);
+dat.color.toString=function(e){return function(a){if(1==a.a||e.isUndefined(a.a)){for(a=a.hex.toString(16);6>a.length;)a="0"+a;return"#"+a}return"rgba("+Math.round(a.r)+","+Math.round(a.g)+","+Math.round(a.b)+","+a.a+")"}}(dat.utils.common);
+dat.color.interpret=function(e,a){var b,d,f=[{litmus:a.isString,conversions:{THREE_CHAR_HEX:{read:function(a){a=a.match(/^#([A-F0-9])([A-F0-9])([A-F0-9])$/i);return null===a?!1:{space:"HEX",hex:parseInt("0x"+a[1].toString()+a[1].toString()+a[2].toString()+a[2].toString()+a[3].toString()+a[3].toString())}},write:e},SIX_CHAR_HEX:{read:function(a){a=a.match(/^#([A-F0-9]{6})$/i);return null===a?!1:{space:"HEX",hex:parseInt("0x"+a[1].toString())}},write:e},CSS_RGB:{read:function(a){a=a.match(/^rgb\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\)/);
+return null===a?!1:{space:"RGB",r:parseFloat(a[1]),g:parseFloat(a[2]),b:parseFloat(a[3])}},write:e},CSS_RGBA:{read:function(a){a=a.match(/^rgba\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\,\s*(.+)\s*\)/);return null===a?!1:{space:"RGB",r:parseFloat(a[1]),g:parseFloat(a[2]),b:parseFloat(a[3]),a:parseFloat(a[4])}},write:e}}},{litmus:a.isNumber,conversions:{HEX:{read:function(a){return{space:"HEX",hex:a,conversionName:"HEX"}},write:function(a){return a.hex}}}},{litmus:a.isArray,conversions:{RGB_ARRAY:{read:function(a){return 3!=
+a.length?!1:{space:"RGB",r:a[0],g:a[1],b:a[2]}},write:function(a){return[a.r,a.g,a.b]}},RGBA_ARRAY:{read:function(a){return 4!=a.length?!1:{space:"RGB",r:a[0],g:a[1],b:a[2],a:a[3]}},write:function(a){return[a.r,a.g,a.b,a.a]}}}},{litmus:a.isObject,conversions:{RGBA_OBJ:{read:function(c){return a.isNumber(c.r)&&a.isNumber(c.g)&&a.isNumber(c.b)&&a.isNumber(c.a)?{space:"RGB",r:c.r,g:c.g,b:c.b,a:c.a}:!1},write:function(a){return{r:a.r,g:a.g,b:a.b,a:a.a}}},RGB_OBJ:{read:function(c){return a.isNumber(c.r)&&
+a.isNumber(c.g)&&a.isNumber(c.b)?{space:"RGB",r:c.r,g:c.g,b:c.b}:!1},write:function(a){return{r:a.r,g:a.g,b:a.b}}},HSVA_OBJ:{read:function(c){return a.isNumber(c.h)&&a.isNumber(c.s)&&a.isNumber(c.v)&&a.isNumber(c.a)?{space:"HSV",h:c.h,s:c.s,v:c.v,a:c.a}:!1},write:function(a){return{h:a.h,s:a.s,v:a.v,a:a.a}}},HSV_OBJ:{read:function(d){return a.isNumber(d.h)&&a.isNumber(d.s)&&a.isNumber(d.v)?{space:"HSV",h:d.h,s:d.s,v:d.v}:!1},write:function(a){return{h:a.h,s:a.s,v:a.v}}}}}];return function(){d=!1;
+var c=1 0&&(q=1e5/Math.pow(10,v),r=a.substring(f,f+v),w=parseFloat(r)*q,s=a.substring(f+v),x=parseFloat(s)*q),t=w+j,u=x+l,{easting:t,northing:u,zoneLetter:h,zoneNumber:g,accuracy:q}}function n(a,b){for(var c=r.charCodeAt(b-1),d=1e5,e=!1;c!==a.charCodeAt(0);){if(c++,c===u&&c++,c===v&&c++,c>x){if(e)throw"Bad character: "+a;c=t,e=!0}d+=1e5}return d}function o(a,b){if(a>"V")throw"MGRSPoint given invalid Northing "+a;for(var c=s.charCodeAt(b-1),d=0,e=!1;c!==a.charCodeAt(0);){if(c++,c===u&&c++,c===v&&c++,c>w){if(e)throw"Bad character: "+a;c=t,e=!0}d+=1e5}return d}function p(a){var b;switch(a){case"C":b=11e5;break;case"D":b=2e6;break;case"E":b=28e5;break;case"F":b=37e5;break;case"G":b=46e5;break;case"H":b=55e5;break;case"J":b=64e5;break;case"K":b=73e5;break;case"L":b=82e5;break;case"M":b=91e5;break;case"N":b=0;break;case"P":b=8e5;break;case"Q":b=17e5;break;case"R":b=26e5;break;case"S":b=35e5;break;case"T":b=44e5;break;case"U":b=53e5;break;case"V":b=62e5;break;case"W":b=7e6;break;case"X":b=79e5;break;default:b=-1}if(b>=0)return b;throw"Invalid zone letter: "+a}var q=6,r="AJSAJS",s="AFAFAF",t=65,u=73,v=79,w=86,x=90;c.forward=function(a,b){return b=b||5,i(f({lat:a[1],lon:a[0]}),b)},c.inverse=function(a){var b=g(m(a.toUpperCase()));return[b.left,b.bottom,b.right,b.top]},c.toPoint=function(a){var b=c.inverse(a);return[(b[2]+b[0])/2,(b[3]+b[1])/2]}},{}],67:[function(a,b){b.exports={name:"proj4",version:"2.2.1",description:"Proj4js is a JavaScript library to transform point coordinates from one coordinate system to another, including datum transformations.",main:"lib/index.js",directories:{test:"test",doc:"docs"},scripts:{test:"./node_modules/istanbul/lib/cli.js test ./node_modules/mocha/bin/_mocha test/test.js"},repository:{type:"git",url:"git://github.com/proj4js/proj4js.git"},author:"",license:"MIT",jam:{main:"dist/proj4.js",include:["dist/proj4.js","README.md","AUTHORS","LICENSE.md"]},devDependencies:{"grunt-cli":"~0.1.13",grunt:"~0.4.2","grunt-contrib-connect":"~0.6.0","grunt-contrib-jshint":"~0.8.0",chai:"~1.8.1",mocha:"~1.17.1","grunt-mocha-phantomjs":"~0.4.0",browserify:"~3.24.5","grunt-browserify":"~1.3.0","grunt-contrib-uglify":"~0.3.2",curl:"git://github.com/cujojs/curl.git",istanbul:"~0.2.4",tin:"~0.4.0"},dependencies:{mgrs:"0.0.0"}}},{}],"./includedProjections":[function(a,b){b.exports=a("gWUPNW")},{}],gWUPNW:[function(a,b){var c=[a("./lib/projections/tmerc"),a("./lib/projections/utm"),a("./lib/projections/sterea"),a("./lib/projections/stere"),a("./lib/projections/somerc"),a("./lib/projections/omerc"),a("./lib/projections/lcc"),a("./lib/projections/krovak"),a("./lib/projections/cass"),a("./lib/projections/laea"),a("./lib/projections/aea"),a("./lib/projections/gnom"),a("./lib/projections/cea"),a("./lib/projections/eqc"),a("./lib/projections/poly"),a("./lib/projections/nzmg"),a("./lib/projections/mill"),a("./lib/projections/sinu"),a("./lib/projections/moll"),a("./lib/projections/eqdc"),a("./lib/projections/vandg"),a("./lib/projections/aeqd")];b.exports=function(proj4){c.forEach(function(a){proj4.Proj.projections.add(a)})}},{"./lib/projections/aea":39,"./lib/projections/aeqd":40,"./lib/projections/cass":41,"./lib/projections/cea":42,"./lib/projections/eqc":43,"./lib/projections/eqdc":44,"./lib/projections/gnom":46,"./lib/projections/krovak":47,"./lib/projections/laea":48,"./lib/projections/lcc":49,"./lib/projections/mill":52,"./lib/projections/moll":53,"./lib/projections/nzmg":54,"./lib/projections/omerc":55,"./lib/projections/poly":56,"./lib/projections/sinu":57,"./lib/projections/somerc":58,"./lib/projections/stere":59,"./lib/projections/sterea":60,"./lib/projections/tmerc":61,"./lib/projections/utm":62,"./lib/projections/vandg":63}]},{},[35])(35)});
\ No newline at end of file
diff --git a/docs/examples/24/qgis2threejs.css b/docs/examples/24/qgis2threejs.css
new file mode 100644
index 00000000..82748424
--- /dev/null
+++ b/docs/examples/24/qgis2threejs.css
@@ -0,0 +1,286 @@
+body {
+ font-family: arial, sans-serif;
+ margin: 0;
+ overflow: hidden;
+}
+
+#view {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+}
+
+#view.sky {
+ background: -webkit-gradient(linear, left top, left bottom, from(#98c8f6), color-stop(0.4, #cbebff), to(#f0f9ff));
+ background: linear-gradient(to bottom, #98c8f6 0%,#cbebff 40%,#f0f9ff 100%);
+}
+
+.hidden {
+ display: none !important;
+}
+
+.queryable {
+ cursor: pointer;
+}
+
+.no-events {
+ pointer-events: none;
+}
+
+#labels {
+}
+
+.label {
+ position: absolute;
+ padding: 2px;
+ text-shadow: -1px -1px #FFF, 0px -1px #FFF, 1px -1px #FFF, -1px 0px #FFF, 1px 0px #FFF, -1px 1px #FFF, 0px 1px #FFF, 1px 1px #FFF;
+}
+
+.print-label {
+ /* these 2 properties are copied to canvas 2D context in renderLabels function of Qgis2threejs.js */
+ color: black;
+ font: normal medium sans-serif;
+}
+
+#progress {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 5px;
+}
+
+#bar {
+ width: 0;
+ height: 5px;
+ background-color: #2196F3;
+ opacity: 1;
+}
+
+#bar.fadeout {
+ opacity: 0;
+ height: 0;
+ transition: all .4s;
+}
+
+#header {
+ position: absolute;
+ left: 5px;
+ top: 5px;
+}
+
+#footer {
+ position: absolute;
+ left: 5px;
+ bottom: 5px;
+}
+
+#infobtn {
+ cursor: pointer;
+}
+
+#infobtn img {
+ width: 24px;
+ height: 24px;
+}
+
+/* popup */
+#popup {
+ position: absolute;
+ left: 2px;
+ top: 2px;
+ max-width: 480px;
+ min-width: 300px;
+ z-index: 9999;
+ border: solid gray;
+ border-width: 1px 2px 2px;
+ border-radius: 3px;
+ display: none;
+}
+
+#popupbar {
+ background: dimgray;
+ color: white;
+ padding-left: 4px;
+ height: 18px;
+ border-radius: 3px 3px 0 0;
+}
+
+#closebtn {
+ color: rgb(220, 220, 220);
+ background: gray;
+ border: 1px solid darkgray;
+ border-radius: 4px;
+ line-height: 16px;
+ font-size: 16px;
+ font-weight: bold;
+ width: 16px;
+ height: 16px;
+ text-align: center;
+ float: right;
+ cursor: pointer;
+}
+
+#popupbody {
+ background-color: white;
+ padding: 2px 5px 2px;
+ border-radius: 0 0 3px 3px;
+ max-height: 500px;
+ overflow: auto;
+}
+
+/* identify result */
+#popupbody table {
+ margin-top: 5px;
+ margin-bottom: 3px;
+ width: 100%;
+}
+
+#popupbody table caption {
+ background-color: gray;
+ color: white;
+ font-size: small;
+ padding: 1px;
+ padding-left: 6px;
+ text-align: left;
+}
+
+#popupbody table td {
+ padding-left: 5px;
+}
+
+#qr_layername_table tr {
+ background-color: #eeeeee;
+}
+
+#qr_coords_table tr {
+ background-color: #eeeeee;
+}
+
+#qr_attrs_table tr:nth-child(odd) {
+ background-color: #eeeeee;
+}
+
+#qr_attrs_table tr td:first-child {
+ width: 40%;
+}
+
+.action-btn {
+ display: inline-block;
+ border: 1px solid gray;
+ border-radius: 2px;
+ background-color: #FBFBFB;
+ cursor: pointer;
+ font-size: small;
+ padding: 1px;
+ margin-left: 2px;
+}
+
+.action-zoom {
+}
+
+.action-move {
+ display: none;
+}
+
+/* pageinfo */
+#pageinfo {
+ font-size: small;
+}
+
+#pageinfo h1 {
+ background-color: gray;
+ color: white;
+ font-size: small;
+ font-weight: normal;
+ margin-top: 5px;
+ margin-bottom: 3px;
+ padding: 1px;
+ padding-left: 6px;
+}
+
+#urlbox {
+ width: 100%;
+}
+
+#usage, #about {
+ margin-left: 5px;
+}
+
+#usage tr:nth-child(odd) {
+ background-color: #eeeeee;
+}
+
+#usage td.star {
+ font-weight: bold;
+}
+
+#about ul {
+ margin: 5px;
+ margin-left: 20px;
+ padding: 0px;
+}
+
+.license {
+ font-size: xx-small;
+}
+
+.download-link {
+ display: block;
+ border: 1px solid darkgray;
+ background: lightgray;
+ color: black;
+ font-size: large;
+ margin: 10px 50px 10px;
+ padding: 8px;
+ text-align: center;
+ text-decoration: none;
+}
+
+/* print dialog */
+.print div {
+ padding: 5px;
+}
+
+.print label {
+ margin-left: 5px;
+}
+
+.print input[type="text"] {
+ width: 40px;
+ border: 1px solid lightgray;
+ background: #F6F6F6;
+ margin-left: 8px;
+ margin-right: 8px;
+}
+
+.print input[type="checkbox"] {
+ margin-left: 15px;
+}
+
+.print input[type="submit"] {
+ display: none;
+}
+
+.print .buttonbox {
+ text-align: center;
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+.print .buttonbox span {
+ border: 1px solid darkgray;
+ background: lightgray;
+ margin: 0px 5px 0px;
+ padding: 5px 10px 5px;
+ cursor: pointer;
+}
+
+#northarrow {
+ position: absolute;
+ left: 12px;
+ bottom: 16px;
+ width: 80px;
+ height: 80px;
+ z-index: 1000;
+}
diff --git a/docs/examples/24/qgis2threejs.js b/docs/examples/24/qgis2threejs.js
new file mode 100644
index 00000000..1f721f5f
--- /dev/null
+++ b/docs/examples/24/qgis2threejs.js
@@ -0,0 +1,3290 @@
+"use strict";
+// Qgis2threejs.js
+// (C) 2014 Minoru Akagi | MIT License
+// https://github.com/minorua/Qgis2threejs
+
+var Q3D = {VERSION: "2.4"};
+
+Q3D.Config = {
+ // scene
+ autoZShift: false, // automatic z shift adjustment
+ bgColor: null, // null is sky
+ // camera
+ orthoCamera: false,
+ viewpoint: { // note: y-up
+ pos: {x: 0, y: 100, z: 100}, // initial camera position
+ lookAt: {x: 0, y: 0, z:0}
+ },
+ // light
+ lights: [
+ {
+ type: "ambient",
+ color: 0x999999,
+ intensity: 0.8
+ },
+ {
+ type: "directional",
+ color: 0xffffff,
+ intensity: 0.7,
+ azimuth: 220, // note: default light azimuth of gdaldem hillshade is 315.
+ altitude: 45 // altitude angle
+ },
+ {
+ type: "directional",
+ color: 0xffffff,
+ intensity: 0.1,
+ azimuth: 40,
+ altitude: -45
+ }
+ ],
+ // layer
+ dem: {
+ side: {
+ bottomZ: -1.5 // in the unit of world coordinates
+ },
+ frame: {
+ bottomZ: -1.5
+ }
+ },
+ line: {
+ dash: {
+ dashSize: 1,
+ gapSize: 0.5
+ }
+ },
+ label: {
+ visible: true,
+ connectorColor: 0xc0c0d0,
+ fixedSize: false,
+ minFontSize: 8,
+ queryable: true
+ },
+ // decoration
+ northArrow: {
+ color: 0x8b4513,
+ cameraDistance: 30,
+ visible: false
+ },
+
+ qmarker: {
+ r: 0.25,
+ c: 0xffff00,
+ o: 0.8
+ },
+ allVisible: false, // set every layer visible property to true on load if set to true
+ debugMode: false,
+ exportMode: false // set to true in glTF export mode
+};
+
+// consts
+Q3D.LayerType = {
+ DEM: "dem",
+ Point: "point",
+ Line: "line",
+ Polygon: "polygon"
+};
+
+Q3D.MaterialType = {
+ MeshLambert: 0,
+ MeshPhong: 1,
+ MeshToon: 2,
+ LineBasic: 3,
+ LineDashed: 4,
+ Sprite: 5,
+ Point: 6,
+ Unknown: -1
+};
+
+Q3D.uv = {
+ i: new THREE.Vector3(1, 0, 0),
+ j: new THREE.Vector3(0, 1, 0),
+ k: new THREE.Vector3(0, 0, 1)
+};
+
+Q3D.ua = window.navigator.userAgent.toLowerCase();
+Q3D.isIE = (Q3D.ua.indexOf("msie") != -1 || Q3D.ua.indexOf("trident") != -1);
+Q3D.isTouchDevice = ("ontouchstart" in window);
+
+Q3D.$ = function (elementId) {
+ return document.getElementById(elementId);
+};
+
+/*
+Q3D.Group -> THREE.Group -> THREE.Object3D
+*/
+Q3D.Group = function () {
+ THREE.Group.call(this);
+};
+
+Q3D.Group.prototype = Object.create(THREE.Group.prototype);
+Q3D.Group.prototype.constructor = Q3D.Group;
+
+Q3D.Group.prototype.add = function (object) {
+ THREE.Group.prototype.add.call(this, object);
+ object.updateMatrixWorld();
+};
+
+Q3D.Group.prototype.clear = function () {
+ for (var i = this.children.length - 1 ; i >= 0; i--) {
+ this.remove(this.children[i]);
+ }
+};
+
+
+/*
+Q3D.Scene -> THREE.Scene -> THREE.Object3D
+
+.mapLayers: an object that holds map layers contained in this scene. the key is layerId.
+ use .loadJSONObject() to add a map layer to this scene.
+.userData: an object that holds metadata (crs, proj, baseExtent, rotation, width, zExaggeration, zShift, wgs84Center)
+ properties of the scene object in JSON data?
+
+.add(object):
+.getObjectByName(layerId): returns the layer object specified by the layer id.
+
+--
+custom function
+.loadJSONObject(json_obj):
+.toMapCoordinates(x, y, z): converts world coordinates to map coordinates
+._rotatePoint(point, degrees, origin):
+*/
+Q3D.Scene = function () {
+ THREE.Scene.call(this);
+ this.autoUpdate = false;
+
+ // scene is z-up
+ this.rotation.x = -Math.PI / 2;
+ this.updateMatrixWorld();
+
+ this.mapLayers = {};
+
+ this.lightGroup = new Q3D.Group();
+ this.add(this.lightGroup);
+
+ this.labelConnectorGroup = new Q3D.Group();
+ this.add(this.labelConnectorGroup);
+
+ this.labelRootElement = null;
+};
+
+Q3D.Scene.prototype = Object.create(THREE.Scene.prototype);
+Q3D.Scene.prototype.constructor = Q3D.Scene;
+
+Q3D.Scene.prototype.add = function (object) {
+ THREE.Scene.prototype.add.call(this, object);
+ object.updateMatrixWorld();
+};
+
+Q3D.Scene.prototype.loadJSONObject = function (jsonObject) {
+ if (jsonObject.type == "scene") {
+ // set properties
+ if (jsonObject.properties !== undefined) {
+ this.userData = jsonObject.properties;
+
+ var w = (this.userData.baseExtent[2] - this.userData.baseExtent[0]),
+ h = (this.userData.baseExtent[3] - this.userData.baseExtent[1]);
+
+ this.userData.scale = this.userData.width / w;
+ this.userData.zScale = this.userData.scale * this.userData.zExaggeration;
+
+ this.userData.origin = {x: this.userData.baseExtent[0] + w / 2,
+ y: this.userData.baseExtent[1] + h / 2,
+ z: -this.userData.zShift};
+ }
+
+ // load lights
+ if (jsonObject.lights !== undefined) {
+ // remove all existing lights
+ this.lightGroup.clear();
+
+ // build lights if scene data has lights settings
+ // [not implemented yet]
+ }
+
+ // build default lights if this scene has no lights yet
+ if (this.lightGroup.children.length == 0) this.buildDefaultLights();
+
+ // load layers
+ if (jsonObject.layers !== undefined) {
+ jsonObject.layers.forEach(function (layer) {
+ this.loadJSONObject(layer);
+ }, this);
+ }
+ }
+ else if (jsonObject.type == "layer") {
+ var layer = this.mapLayers[jsonObject.id];
+ if (layer === undefined) {
+ // console.assert(jsonObject.properties !== undefined);
+
+ // create a layer
+ var type = jsonObject.properties.type;
+ if (type == "dem") layer = new Q3D.DEMLayer();
+ else if (type == "point") layer = new Q3D.PointLayer();
+ else if (type == "line") layer = new Q3D.LineLayer();
+ else if (type == "polygon") layer = new Q3D.PolygonLayer();
+ else {
+ // console.error("unknown layer type:" + type);
+ return;
+ }
+ layer.id = jsonObject.id;
+ layer.addEventListener("renderRequest", this.requestRender.bind(this));
+
+ this.mapLayers[jsonObject.id] = layer;
+ this.add(layer.objectGroup);
+ }
+
+ layer.loadJSONObject(jsonObject, this);
+
+ this.requestRender();
+ }
+ else if (jsonObject.type == "block") {
+ var layer = this.mapLayers[jsonObject.layer];
+ if (layer === undefined) {
+ // console.error("layer not exists:" + jsonObject.layer);
+ return;
+ }
+ layer.loadJSONObject(jsonObject, this);
+
+ this.requestRender();
+ }
+};
+
+Q3D.Scene.prototype.buildLights = function (lights) {
+ var p, light, lambda, phi, x, y, z;
+ var deg2rad = Math.PI / 180;
+ for (var i = 0; i < lights.length; i++) {
+ p = lights[i];
+ if (p.type == "ambient") {
+ this.lightGroup.add(new THREE.AmbientLight(p.color, p.intensity));
+ }
+ else if (p.type == "directional") {
+ lambda = (90 - p.azimuth) * deg2rad;
+ phi = p.altitude * deg2rad;
+
+ x = Math.cos(phi) * Math.cos(lambda);
+ y = Math.cos(phi) * Math.sin(lambda);
+ z = Math.sin(phi);
+
+ light = new THREE.DirectionalLight(p.color, p.intensity);
+ light.position.set(x, y, z);
+ this.lightGroup.add(light);
+ }
+ }
+};
+
+Q3D.Scene.prototype.buildDefaultLights = function () {
+ this.buildLights(Q3D.Config.lights);
+};
+
+Q3D.Scene.prototype.requestRender = function () {
+ this.dispatchEvent({type: "renderRequest"});
+};
+
+Q3D.Scene.prototype.queryableObjects = function () {
+ var objs = [];
+ for (var id in this.mapLayers) {
+ objs = objs.concat(this.mapLayers[id].queryableObjects());
+ }
+ return objs;
+};
+
+Q3D.Scene.prototype.toMapCoordinates = function (x, y, z) {
+ if (this.userData.rotation) {
+ var pt = this._rotatePoint({x: x, y: y}, this.userData.rotation);
+ x = pt.x;
+ y = pt.y;
+ }
+ return {x: x / this.userData.scale + this.userData.origin.x,
+ y: y / this.userData.scale + this.userData.origin.y,
+ z: z / this.userData.zScale + this.userData.origin.z};
+};
+
+// real (geodetic/projected) coordinates to 3D scene coordinates
+Q3D.Scene.prototype.toLocalCoordinates = function (x, y, z, isProjected) {
+ // project x and y coordinates from WGS84 (long, lat)
+ var pt;
+ if (!isProjected && typeof proj4 !== "undefined") {
+ pt = proj4(this.userData.proj).forward([x, y]);
+ x = pt[0];
+ y = pt[1];
+ }
+
+ x = (x - this.userData.origin.x) * this.userData.scale;
+ y = (y - this.userData.origin.y) * this.userData.scale;
+ z = (z - this.userData.origin.z) * this.userData.zScale;
+
+ if (this.userData.rotation) {
+ pt = this._rotatePoint({x: x, y: y}, -this.userData.rotation);
+ x = pt.x;
+ y = pt.y;
+ }
+ return {x: x, y: y, z: z};
+};
+
+// Rotate a point counter-clockwise around an origin
+Q3D.Scene.prototype._rotatePoint = function (point, degrees, origin) {
+ var theta = degrees * Math.PI / 180,
+ c = Math.cos(theta),
+ s = Math.sin(theta),
+ x = point.x,
+ y = point.y;
+
+ if (origin) {
+ x -= origin.x;
+ y -= origin.y;
+ }
+
+ var xd = x * c - y * s,
+ yd = x * s + y * c;
+
+ if (origin) {
+ xd += origin.x;
+ yd += origin.y;
+ }
+ return {x: xd, y: yd};
+};
+
+Q3D.Scene.prototype.adjustZShift = function () {
+ // initialize
+ this.userData.zShiftA = 0;
+ this.position.y = 0;
+ this.updateMatrixWorld();
+
+ var box = new THREE.Box3();
+ for (var id in this.mapLayers) {
+ if (this.mapLayers[id].visible) {
+ box.union(this.mapLayers[id].boundingBox());
+ }
+ }
+
+ // bbox zmin in map coordinates
+ var zmin = (box.min.y === Infinity) ? 0 : (box.min.y / this.userData.zScale - this.userData.zShift);
+
+ // shift scene so that bbox zmin becomes zero
+ this.userData.zShiftA = -zmin;
+ this.position.y = this.userData.zShiftA * this.userData.zScale;
+
+ // keep positions of lights in world coordinates
+ this.lightGroup.position.z = -this.position.y;
+
+ this.updateMatrixWorld();
+
+ this.userData.origin.z = -(this.userData.zShift + this.userData.zShiftA);
+
+ console.log("z shift adjusted: " + this.userData.zShiftA);
+
+ this.dispatchEvent({type: "zShiftAdjusted", sceneData: this.userData});
+
+ this.requestRender();
+};
+
+
+/*
+Q3D.application
+
+limitations:
+- one renderer
+- one scene
+*/
+(function () {
+ // the application
+ var app = {};
+ Q3D.application = app;
+
+ var vec3 = new THREE.Vector3();
+
+ var listeners = {};
+ var dispatchEvent = function (event) {
+ if (Q3D.Config.debugMode) console.log("about to dispatch " + event + " event.");
+
+ var ls = listeners[event.type] || [];
+ for (var i = 0; i < ls.length; i++) {
+ ls[i](event);
+ }
+ };
+
+ app.addEventListener = function (type, listener, prepend) {
+ listeners[type] = listeners[type] || [];
+ if (prepend) {
+ listeners[type].unshift(listener);
+ }
+ else {
+ listeners[type].push(listener);
+ }
+ };
+
+ app.init = function (container) {
+ app.container = container;
+ app.running = false; // if true, animation loop is continued.
+
+ // URL parameters
+ app.urlParams = app.parseUrlParameters();
+ if ("popup" in app.urlParams) {
+ // open popup window
+ var c = window.location.href.split("?");
+ window.open(c[0] + "?" + c[1].replace(/&?popup/, ""), "popup", "width=" + app.urlParams.width + ",height=" + app.urlParams.height);
+ app.popup.show("Another window has been opened.");
+ return;
+ }
+
+ if (app.urlParams.width && app.urlParams.height) {
+ // set container size
+ container.style.width = app.urlParams.width + "px";
+ container.style.height = app.urlParams.height + "px";
+ }
+
+ app.width = container.clientWidth;
+ app.height = container.clientHeight;
+
+ var bgcolor = Q3D.Config.bgColor;
+ if (bgcolor === null) container.classList.add("sky");
+
+ // WebGLRenderer
+ app.renderer = new THREE.WebGLRenderer({alpha: true, antialias: true});
+ app.renderer.setSize(app.width, app.height);
+ app.renderer.setClearColor(bgcolor || 0, (bgcolor === null) ? 0 : 1);
+ app.container.appendChild(app.renderer.domElement);
+
+ // set viewpoint if specified by URL parameters
+ var vars = app.urlParams;
+ if (vars.cx !== undefined) Q3D.Config.viewpoint.pos = {x: parseFloat(vars.cx), y: parseFloat(vars.cy), z: parseFloat(vars.cz)};
+ if (vars.tx !== undefined) Q3D.Config.viewpoint.lookAt = {x: parseFloat(vars.tx), y: parseFloat(vars.ty), z: parseFloat(vars.tz)};
+
+ // camera
+ app.buildCamera(Q3D.Config.orthoCamera);
+
+ // scene
+ app.scene = new Q3D.Scene();
+ app.scene.addEventListener("renderRequest", function (event) {
+ app.render();
+ });
+
+ var controls;
+ if (typeof THREE.OrbitControls !== "undefined") {
+ controls = new THREE.OrbitControls(app.camera, app.renderer.domElement);
+ controls.enableKeys = false;
+
+ var t = Q3D.Config.viewpoint.lookAt;
+ controls.target.set(t.x, t.y, t.z);
+
+ // custom functions
+ var offset = new THREE.Vector3();
+ var spherical = new THREE.Spherical();
+
+ controls.moveForward = function (delta) {
+ offset.copy(controls.object.position).sub(controls.target);
+ var targetDistance = offset.length() * Math.tan((controls.object.fov / 2) * Math.PI / 180.0);
+ offset.y = 0;
+ offset.normalize();
+ offset.multiplyScalar(-2 * delta * targetDistance / app.renderer.domElement.clientHeight);
+
+ controls.object.position.add(offset);
+ controls.target.add(offset);
+ };
+ controls.cameraRotate = function (thetaDelta, phiDelta) {
+ offset.copy(controls.target).sub(controls.object.position);
+ spherical.setFromVector3(offset);
+
+ spherical.theta += thetaDelta;
+ spherical.phi -= phiDelta;
+
+ // restrict theta/phi to be between desired limits
+ spherical.theta = Math.max(controls.minAzimuthAngle, Math.min(controls.maxAzimuthAngle, spherical.theta));
+ spherical.phi = Math.max(controls.minPolarAngle, Math.min(controls.maxPolarAngle, spherical.phi));
+ spherical.makeSafe();
+
+ offset.setFromSpherical(spherical);
+ controls.target.copy(controls.object.position).add(offset);
+ controls.object.lookAt(controls.target);
+ };
+
+ controls.addEventListener("change", function (event) {
+ app.render();
+ });
+ }
+
+ app.controls = controls;
+ app.controls.update();
+
+ app.labelVisible = Q3D.Config.label.visible;
+
+ // root element of labels
+ app.scene.labelRootElement = document.getElementById("labels");
+ app.scene.labelRootElement.style.display = (app.labelVisible) ? "block" : "none";
+
+ // create a marker for queried point
+ var opt = Q3D.Config.qmarker;
+ app.queryMarker = new THREE.Mesh(new THREE.SphereBufferGeometry(opt.r),
+ new THREE.MeshLambertMaterial({color: opt.c, opacity: opt.o, transparent: (opt.o < 1)}));
+
+ app.highlightMaterial = new THREE.MeshLambertMaterial({emissive: 0x999900, transparent: true, opacity: 0.5});
+ if (!Q3D.isIE) app.highlightMaterial.side = THREE.DoubleSide; // Shader compilation error occurs with double sided material on IE11
+
+ app.selectedObject = null;
+ app.highlightObject = null;
+
+ app.modelBuilders = [];
+ app._wireframeMode = false;
+
+ // add event listeners
+ app.addEventListener("sceneLoaded", function () {
+ if (Q3D.Config.autoZShift) {
+ app.scene.adjustZShift();
+ }
+ app.render();
+ }, true);
+
+ window.addEventListener("keydown", app.eventListener.keydown);
+ window.addEventListener("resize", app.eventListener.resize);
+
+ app.renderer.domElement.addEventListener("mousedown", app.eventListener.mousedown);
+ app.renderer.domElement.addEventListener("mouseup", app.eventListener.mouseup);
+
+ var e = Q3D.$("closebtn");
+ if (e) e.addEventListener("click", app.closePopup);
+ };
+
+ app.parseUrlParameters = function () {
+ var p, vars = {};
+ var params = window.location.search.substring(1).split('&').concat(window.location.hash.substring(1).split('&'));
+ params.forEach(function (param) {
+ p = param.split('=');
+ vars[p[0]] = p[1];
+ });
+ return vars;
+ };
+
+ app.initLoadingManager = function () {
+ if (app.loadingManager) {
+ app.loadingManager.onLoad = app.loadingManager.onProgress = app.loadingManager.onError = undefined;
+ }
+
+ app.loadingManager = new THREE.LoadingManager(function () {
+ // onLoad
+ app.loadingManager.isLoading = false;
+
+ document.getElementById("bar").classList.add("fadeout");
+
+ dispatchEvent({type: "sceneLoaded"});
+ },
+ function (url, loaded, total) {
+ // onProgress
+ document.getElementById("bar").style.width = (loaded / total * 100) + "%";
+ },
+ function () {
+ app.loadingManager.isLoading = false;
+
+ dispatchEvent({type: "sceneLoadError"});
+ });
+
+ app.loadingManager.onStart = function () {
+ app.loadingManager.isLoading = true;
+ };
+
+ app.loadingManager.isLoading = false;
+ };
+
+ app.initLoadingManager();
+
+ app.loadFile = function (url, type, callback) {
+
+ var loader = new THREE.FileLoader(app.loadingManager);
+ loader.setResponseType(type);
+
+ var onError = function (e) {
+ if (location.protocol == "file:") {
+ app.popup.show("This browser doesn't allow loading local files via Ajax. See plugin wiki page for details.", "Error", true);
+ }
+ };
+
+ try {
+ loader.load(url, callback, undefined, onError);
+ }
+ catch (e) { // for IE
+ onError(e);
+ }
+ };
+
+ app.loadJSONObject = function (jsonObject) {
+ app.scene.loadJSONObject(jsonObject);
+ };
+
+ app.loadJSONFile = function (url, callback) {
+ app.loadFile(url, "json", function (obj) {
+ app.loadJSONObject(obj);
+ if (callback) callback(obj);
+ });
+ };
+
+ app.loadSceneFile = function (url, callback) {
+ var ext = url.split(".").pop();
+ if (ext == "json") app.loadJSONFile(url, callback);
+ else if (ext == "js") {
+ var e = document.createElement("script");
+ e.src = url;
+ e.onload = callback;
+ document.body.appendChild(e);
+ }
+ };
+
+ app.loadTextureFile = function (url, callback) {
+ return new THREE.TextureLoader(app.loadingManager).load(url, callback);
+ };
+
+ app.loadModelFile = function (url, callback) {
+ var loader,
+ ext = url.split(".").pop();
+ if (ext == "dae") {
+ loader = new THREE.ColladaLoader(app.loadingManager);
+ }
+ else if (ext == "gltf" || ext == "glb") {
+ loader = new THREE.GLTFLoader(app.loadingManager);
+ }
+ else {
+ console.log("Model file type not supported: " + url);
+ return;
+ }
+
+ app.loadingManager.itemStart("M" + url);
+
+ loader.load(url, function (model) {
+ if (callback) callback(model);
+ app.loadingManager.itemEnd("M" + url);
+ },
+ undefined,
+ function (e) {
+ console.log("Failed to load model: " + url);
+ app.loadingManager.itemError("M" + url);
+ });
+ };
+
+ app.mouseDownPoint = new THREE.Vector2();
+ app.mouseUpPoint = new THREE.Vector2();
+
+ app.eventListener = {
+
+ keydown: function (e) {
+ var controls = app.controls;
+ var panDelta = 3, rotateAngle = 2 * Math.PI / 180;
+ if (e.shiftKey && e.ctrlKey) {
+ switch (e.keyCode) {
+ case 38: // Shift + Ctrl + UP
+ controls.dollyOut(controls.getZoomScale());
+ break;
+ case 40: // Shift + Ctrl + DOWN
+ controls.dollyIn(controls.getZoomScale());
+ break;
+ default:
+ return;
+ }
+ }
+ else if (e.shiftKey) {
+ switch (e.keyCode) {
+ case 37: // LEFT
+ controls.rotateLeft(rotateAngle);
+ break;
+ case 38: // UP
+ controls.rotateUp(rotateAngle);
+ break;
+ case 39: // RIGHT
+ controls.rotateLeft(-rotateAngle);
+ break;
+ case 40: // DOWN
+ controls.rotateUp(-rotateAngle);
+ break;
+ case 82: // Shift + R
+ controls.reset();
+ break;
+ case 83: // Shift + S
+ app.showPrintDialog();
+ return;
+ default:
+ return;
+ }
+ }
+ else if (e.ctrlKey) {
+ switch (e.keyCode) {
+ case 37: // Ctrl + LEFT
+ controls.cameraRotate(rotateAngle, 0);
+ break;
+ case 38: // Ctrl + UP
+ controls.cameraRotate(0, rotateAngle);
+ break;
+ case 39: // Ctrl + RIGHT
+ controls.cameraRotate(-rotateAngle, 0);
+ break;
+ case 40: // Ctrl + DOWN
+ controls.cameraRotate(0, -rotateAngle);
+ break;
+ default:
+ return;
+ }
+ }
+ else {
+ switch (e.keyCode) {
+ case 37: // LEFT
+ controls.panLeft(panDelta, controls.object.matrix);
+ break;
+ case 38: // UP
+ controls.moveForward(3 * panDelta); // horizontally forward
+ break;
+ case 39: // RIGHT
+ controls.panLeft(-panDelta, controls.object.matrix);
+ break;
+ case 40: // DOWN
+ controls.moveForward(-3 * panDelta);
+ break;
+ case 27: // ESC
+ if (Q3D.$("popup").style.display != "none") {
+ app.closePopup();
+ }
+ else if (app.controls.autoRotate) {
+ app.setRotateAnimationMode(false);
+ }
+ return;
+ case 73: // I
+ app.showInfo();
+ return;
+ case 76: // L
+ app.setLabelVisible(!app.labelVisible);
+ return;
+ case 82: // R
+ app.setRotateAnimationMode(!controls.autoRotate);
+ return;
+ case 87: // W
+ app.setWireframeMode(!app._wireframeMode);
+ return;
+ default:
+ return;
+ }
+ }
+ app.controls.update();
+ },
+
+ mousedown: function (e) {
+ app.mouseDownPoint.set(e.clientX, e.clientY);
+ },
+
+ mouseup: function (e) {
+ app.mouseUpPoint.set(e.clientX, e.clientY);
+ if (app.mouseDownPoint.equals(app.mouseUpPoint)) app.canvasClicked(e);
+ },
+
+ resize: function () {
+ app.setCanvasSize(app.container.clientWidth, app.container.clientHeight);
+ app.render();
+ }
+
+ };
+
+ app.setCanvasSize = function (width, height) {
+ app.width = width;
+ app.height = height;
+ app.camera.aspect = width / height;
+ app.camera.updateProjectionMatrix();
+ app.renderer.setSize(width, height);
+ };
+
+ app.buildCamera = function (is_ortho) {
+ if (is_ortho) {
+ app.camera = new THREE.OrthographicCamera(-app.width / 10, app.width / 10, app.height / 10, -app.height / 10, 0.1, 10000);
+ }
+ else {
+ app.camera = new THREE.PerspectiveCamera(45, app.width / app.height, 0.1, 10000);
+ }
+
+ var v = Q3D.Config.viewpoint,
+ p = v.pos,
+ t = v.lookAt;
+ app.camera.position.set(p.x, p.y, p.z);
+ app.camera.lookAt(t.x, t.y, t.z);
+ };
+
+ // rotation: direction to North (clockwise from up (+y), in degrees)
+ app.buildNorthArrow = function (container, rotation) {
+ app.renderer2 = new THREE.WebGLRenderer({alpha: true, antialias: true});
+ app.renderer2.setClearColor(0, 0);
+ app.renderer2.setSize(container.clientWidth, container.clientHeight);
+
+ app.container2 = container;
+ app.container2.appendChild(app.renderer2.domElement);
+
+ app.camera2 = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 1, 1000);
+ app.camera2.up = app.camera.up;
+
+ app.scene2 = new Q3D.Scene();
+ app.scene2.buildDefaultLights();
+
+ // an arrow object
+ var geometry = new THREE.Geometry();
+ geometry.vertices.push(
+ new THREE.Vector3(-5, -10, 0),
+ new THREE.Vector3(0, 10, 0),
+ new THREE.Vector3(0, -7, 3),
+ new THREE.Vector3(5, -10, 0)
+ );
+ geometry.faces.push(
+ new THREE.Face3(0, 1, 2),
+ new THREE.Face3(2, 1, 3)
+ );
+ geometry.computeFaceNormals();
+
+ var material = new THREE.MeshLambertMaterial({color: Q3D.Config.northArrow.color, side: THREE.DoubleSide});
+ var mesh = new THREE.Mesh(geometry, material);
+ if (rotation) mesh.rotation.z = -rotation * Math.PI / 180;
+ app.scene2.add(mesh);
+ };
+
+ app.currentViewUrl = function () {
+ var c = app.camera.position, t = app.controls.target, u = app.camera.up;
+ var hash = "#cx=" + c.x + "&cy=" + c.y + "&cz=" + c.z;
+ if (t.x || t.y || t.z) hash += "&tx=" + t.x + "&ty=" + t.y + "&tz=" + t.z;
+ return window.location.href.split("#")[0] + hash;
+ };
+
+ // enable the controls
+ app.start = function () {
+ if (app.controls) app.controls.enabled = true;
+ };
+
+ app.pause = function () {
+ app.running = false;
+ if (app.controls) app.controls.enabled = false;
+ };
+
+ app.resume = function () {
+ if (app.controls) app.controls.enabled = true;
+ };
+
+ app.startAnimation = function () {
+ app.running = true;
+ app.animate();
+ };
+
+ app.stopAnimation = function () {
+ app.running = false;
+ };
+
+ // animation loop
+ app.animate = function () {
+ if (app.running) requestAnimationFrame(app.animate);
+ app.render(true);
+ };
+
+ app.render = function (updateControls) {
+ if (updateControls) app.controls.update();
+ app.renderer.render(app.scene, app.camera);
+
+ // North arrow
+ if (app.renderer2) {
+ app.camera.getWorldDirection(vec3);
+ app.camera2.position.copy(vec3.negate().setLength(Q3D.Config.northArrow.cameraDistance));
+ app.camera2.quaternion.copy(app.camera.quaternion);
+
+ app.renderer2.render(app.scene2, app.camera2);
+ }
+
+ // labels
+ app.updateLabelPosition();
+ };
+
+ // TODO: remove [obsolete] app.setIntervalRender
+ (function () {
+ var _delay, _repeat, _times, _id = null;
+ var func = function () {
+ app.render();
+ if (_repeat <= ++_times) {
+ clearInterval(_id);
+ _id = null;
+ }
+ };
+ app.setIntervalRender = function (delay, repeat) {
+ if (_id === null || _delay != delay) {
+ if (_id !== null) {
+ clearInterval(_id);
+ }
+ _id = setInterval(func, delay);
+ _delay = delay;
+ }
+ _repeat = repeat;
+ _times = 0;
+ };
+ })();
+
+ // app.updateLabelPosition()
+ (function () {
+ var camera,
+ c2t = new THREE.Vector3(),
+ c2l = new THREE.Vector3();
+
+ app.updateLabelPosition = function () {
+ var rootGroup = app.scene.labelConnectorGroup;
+ if (!app.labelVisible || rootGroup.children.length == 0) return;
+
+ camera = app.camera;
+ camera.getWorldDirection(c2t);
+
+ // make list of [connector object, pt, distance to camera]
+ var obj_dist = [],
+ i, l, k, m,
+ connGroup, conn, pt0;
+
+ for (i = 0, l = rootGroup.children.length; i < l; i++) {
+ connGroup = rootGroup.children[i];
+ if (!connGroup.visible) continue;
+ for (k = 0, m = connGroup.children.length; k < m; k++) {
+ conn = connGroup.children[k];
+ pt0 = conn.geometry.vertices[0];
+ vec3.set(pt0.x, pt0.z, -pt0.y);
+
+ if (c2l.subVectors(vec3, camera.position).dot(c2t) > 0) // label is in front
+ obj_dist.push([conn, pt0, camera.position.distanceTo(vec3)]);
+ else // label is in back
+ conn.userData.elem.style.display = "none";
+ }
+ }
+
+ if (obj_dist.length == 0) return;
+
+ // sort label objects in descending order of distances
+ obj_dist.sort(function (a, b) {
+ if (a[2] < b[2]) return 1;
+ if (a[2] > b[2]) return -1;
+ return 0;
+ });
+
+ var widthHalf = app.width / 2,
+ heightHalf = app.height / 2,
+ fixedSize = Q3D.Config.label.fixedSize,
+ minFontSize = Q3D.Config.label.minFontSize;
+
+ var label, dist, x, y, e, t, fontSize;
+ for (i = 0, l = obj_dist.length; i < l; i++) {
+ label = obj_dist[i][0];
+ pt0 = obj_dist[i][1];
+ dist = obj_dist[i][2];
+
+ // calculate label position
+ vec3.set(pt0.x, pt0.z, -pt0.y).project(camera);
+ x = (vec3.x * widthHalf) + widthHalf;
+ y = -(vec3.y * heightHalf) + heightHalf;
+
+ // set label position
+ e = label.userData.elem;
+ t = "translate(" + (x - (e.offsetWidth / 2)) + "px," + (y - (e.offsetHeight / 2)) + "px)";
+ e.style.display = "block";
+ e.style.zIndex = i + 1;
+ e.style.webkitTransform = t;
+ e.style.transform = t;
+
+ // set font size
+ if (!fixedSize) {
+ if (dist < 10) dist = 10;
+ fontSize = Math.max(Math.round(1000 / dist), minFontSize);
+ e.style.fontSize = fontSize + "px";
+ }
+ }
+ };
+ })();
+
+ app.setLabelVisible = function (visible) {
+ app.labelVisible = visible;
+ app.scene.labelRootElement.style.display = (visible) ? "block" : "none";
+ app.scene.labelConnectorGroup.visible = visible;
+ app.render();
+ };
+
+ app.setRotateAnimationMode = function (enabled) {
+ app.controls.autoRotate = enabled;
+ if (enabled) {
+ app.startAnimation();
+ }
+ else {
+ app.stopAnimation();
+ }
+ };
+
+ app.setWireframeMode = function (wireframe) {
+ if (wireframe == app._wireframeMode) return;
+
+ for (var id in app.scene.mapLayers) {
+ app.scene.mapLayers[id].setWireframeMode(wireframe);
+ }
+
+ app._wireframeMode = wireframe;
+ app.render();
+ };
+
+ app.intersectObjects = function (offsetX, offsetY) {
+ var vec2 = new THREE.Vector2((offsetX / app.width) * 2 - 1,
+ -(offsetY / app.height) * 2 + 1);
+ var ray = new THREE.Raycaster();
+ ray.linePrecision = 0.2;
+ ray.setFromCamera(vec2, app.camera);
+ return ray.intersectObjects(app.scene.queryableObjects());
+ };
+
+ app._offset = function (elm) {
+ var top = 0, left = 0;
+ do {
+ top += elm.offsetTop || 0; left += elm.offsetLeft || 0; elm = elm.offsetParent;
+ } while (elm);
+ return {top: top, left: left};
+ };
+
+ app.popup = {
+
+ timerId: null,
+
+ modal: false,
+
+ // show box
+ // obj: html, element or content id ("queryresult" or "pageinfo")
+ // modal: boolean
+ // duration: int [milliseconds]
+ show: function (obj, title, modal, duration) {
+
+ if (modal) app.pause();
+ else if (this.modal) app.resume();
+
+ this.modal = Boolean(modal);
+
+ var content = Q3D.$("popupcontent");
+ [content, Q3D.$("queryresult"), Q3D.$("pageinfo")].forEach(function (e) {
+ if (e) e.style.display = "none";
+ });
+
+ if (obj == "queryresult" || obj == "pageinfo") {
+ Q3D.$(obj).style.display = "block";
+ }
+ else {
+ if (obj instanceof HTMLElement) {
+ content.innerHTML = "";
+ content.appendChild(obj);
+ }
+ else {
+ content.innerHTML = obj;
+ }
+ content.style.display = "block";
+ }
+ Q3D.$("popupbar").innerHTML = title || "";
+ Q3D.$("popup").style.display = "block";
+
+ if (app.popup.timerId !== null) {
+ clearTimeout(app.popup.timerId);
+ app.popup.timerId = null;
+ }
+
+ if (duration) {
+ app.popup.timerId = setTimeout(function () {
+ app.popup.hide();
+ }, duration);
+ }
+ },
+
+ hide: function () {
+ Q3D.$("popup").style.display = "none";
+ if (app.popup.timerId !== null) clearTimeout(app.popup.timerId);
+ app.popup.timerId = null;
+ if (this.modal) app.resume();
+ }
+
+ };
+
+ app.showInfo = function () {
+ var url = Q3D.$("urlbox");
+ if (url) url.value = app.currentViewUrl();
+ app.popup.show("pageinfo");
+ };
+
+ app.queryTargetPosition = new THREE.Vector3(); // y-up
+
+ app.cameraAction = {
+
+ move: function (x, y, z) { // z-up
+ if (x === undefined) app.camera.position.copy(app.queryTargetPosition);
+ else app.camera.position.set(x, z, -y); // y-up
+ app.render(true);
+ },
+
+ vecZoom: new THREE.Vector3(0, 10, 10), // y-up
+
+ zoomIn: function (x, y, z) { // z-up
+ if (x === undefined) vec3.copy(app.queryTargetPosition);
+ else vec3.set(x, z, -y); // y-up
+
+ app.camera.position.addVectors(vec3, app.cameraAction.vecZoom);
+ app.camera.lookAt(vec3.x, vec3.y, vec3.z);
+ if (app.controls.target !== undefined) app.controls.target.copy(vec3);
+ app.render(true);
+ },
+
+ orbit: function (x, y, z) { // z-up
+ if (app.controls.target === undefined) return;
+
+ if (x === undefined) app.controls.target.copy(app.queryTargetPosition);
+ else app.controls.target.set(x, z, -y); // y-up
+ app.setRotateAnimationMode(true);
+ }
+
+ };
+
+ app.showQueryResult = function (point, obj, hide_coords) {
+ app.queryTargetPosition.set(point.x, point.z, -point.y); // y-up
+
+ var layer = app.scene.mapLayers[obj.userData.layerId],
+ e = document.getElementById("qr_layername");
+
+ // layer name
+ if (layer && e) e.innerHTML = layer.properties.name;
+
+ // clicked coordinates
+ e = document.getElementById("qr_coords_table");
+ if (e) {
+ if (hide_coords) {
+ e.classList.add("hidden");
+ }
+ else {
+ e.classList.remove("hidden");
+
+ var pt = app.scene.toMapCoordinates(point.x, point.y, point.z);
+ e = document.getElementById("qr_coords");
+ if (typeof proj4 === "undefined") {
+ e.innerHTML = [pt.x.toFixed(2), pt.y.toFixed(2), pt.z.toFixed(2)].join(", ");
+ }
+ else {
+ var lonLat = proj4(app.scene.userData.proj).inverse([pt.x, pt.y]);
+ e.innerHTML = Q3D.Utils.convertToDMS(lonLat[1], lonLat[0]) + ", Elev. " + pt.z.toFixed(2);
+ }
+ }
+ }
+
+ e = document.getElementById("qr_attrs_table");
+ if (e) {
+ for (var i = e.children.length - 1; i >= 0; i--) {
+ if (e.children[i].tagName.toUpperCase() == "TR") e.removeChild(e.children[i]);
+ }
+
+ if (layer && layer.properties.propertyNames !== undefined) {
+ var row;
+ for (var i = 0, l = layer.properties.propertyNames.length; i < l; i++) {
+ row = document.createElement("tr");
+ row.innerHTML = "GUI
\'s constructor:\n\n \n\n localStorage
on exit.\n\n localStorage
will\n override those passed to dat.GUI
\'s constructor. This makes it\n easier to work incrementally, but localStorage
is fragile,\n and your friends may not see the same values you do.\n \n
+
+
+
+
+
+
+
+
+
+
+ Current View URL
+
+
+ Usage
+
+
+
+
+ Mouse
+ Left button + Move Orbit
+ Mouse Wheel Zoom
+
+ Right button + Move Pan
+ Keys
+ Arrow keys Move Horizontally
+ Shift + Arrow keys Orbit
+ Ctrl + Arrow keys Rotate
+ Shift + Ctrl + Up / Down Zoom In / Out
+ L Toggle Label Visibility
+ R Start / Stop Rotate Animation (Orbiting)
+ W Wireframe Mode
+ Shift + R Reset Camera Position
+ Shift + S Save Image About
+
+
+
+
+
+
+
+
+
+
+
+ Current View URL
+
+
+ Usage
+
+
+
+
+ Mouse
+ Left button + Move Orbit
+ Mouse Wheel Zoom
+
+ Right button + Move Pan
+ Keys
+ Arrow keys Move Horizontally
+ Shift + Arrow keys Orbit
+ Ctrl + Arrow keys Rotate
+ Shift + Ctrl + Up / Down Zoom In / Out
+ L Toggle Label Visibility
+ R Start / Stop Rotate Animation (Orbiting)
+ W Wireframe Mode
+ Shift + R Reset Camera Position
+ Shift + S Save Image About
+ " + layer.properties.propertyNames[i] + " " +
+ "" + obj.userData.properties[i] + " ";
+ e.appendChild(row);
+ }
+ e.classList.remove("hidden");
+ }
+ else {
+ e.classList.add("hidden");
+ }
+ }
+ app.popup.show("queryresult");
+ };
+
+ app.showPrintDialog = function () {
+
+ function e(tagName, parent, innerHTML) {
+ var elem = document.createElement(tagName);
+ if (parent) parent.appendChild(elem);
+ if (innerHTML) elem.innerHTML = innerHTML;
+ return elem;
+ }
+
+ var f = e("form");
+ f.className = "print";
+
+ var d1 = e("div", f, "Image Size");
+ d1.style.textDecoration = "underline";
+
+ var d2 = e("div", f),
+ l1 = e("label", d2, "Width:"),
+ width = e("input", d2);
+ d2.style.cssFloat = "left";
+ l1.htmlFor = width.id = width.name = "printwidth";
+ width.type = "text";
+ width.value = app.width;
+ e("span", d2, "px,");
+
+ var d3 = e("div", f),
+ l2 = e("label", d3, "Height:"),
+ height = e("input", d3);
+ l2.htmlFor = height.id = height.name = "printheight";
+ height.type = "text";
+ height.value = app.height;
+ e("span", d3, "px");
+
+ var d4 = e("div", f),
+ ka = e("input", d4);
+ ka.type = "checkbox";
+ ka.checked = true;
+ e("span", d4, "Keep Aspect Ratio");
+
+ var d5 = e("div", f, "Option");
+ d5.style.textDecoration = "underline";
+
+ var d6 = e("div", f),
+ bg = e("input", d6);
+ bg.type = "checkbox";
+ bg.checked = true;
+ e("span", d6, "Fill Background");
+
+ var d7 = e("div", f),
+ ok = e("span", d7, "OK"),
+ cancel = e("span", d7, "Cancel");
+ d7.className = "buttonbox";
+
+ e("input", f).type = "submit";
+
+ // event handlers
+ // width and height boxes
+ var aspect = app.width / app.height;
+
+ width.oninput = function () {
+ if (ka.checked) height.value = Math.round(width.value / aspect);
+ };
+
+ height.oninput = function () {
+ if (ka.checked) width.value = Math.round(height.value * aspect);
+ };
+
+ ok.onclick = function () {
+ app.popup.show("Rendering...");
+ window.setTimeout(function () {
+ app.saveCanvasImage(width.value, height.value, bg.checked);
+ }, 10);
+ };
+
+ cancel.onclick = app.closePopup;
+
+ // enter key pressed
+ f.onsubmit = function () {
+ ok.onclick();
+ return false;
+ };
+
+ app.popup.show(f, "Save Image", true); // modal
+ };
+
+ app.closePopup = function () {
+ app.popup.hide();
+ app.scene.remove(app.queryMarker);
+ app.highlightFeature(null);
+ app.render();
+ if (app._canvasImageUrl) {
+ URL.revokeObjectURL(app._canvasImageUrl);
+ app._canvasImageUrl = null;
+ }
+ };
+
+ app.highlightFeature = function (object) {
+ if (app.highlightObject) {
+ // remove highlight object from the scene
+ app.scene.remove(app.highlightObject);
+ app.selectedObject = null;
+ app.highlightObject = null;
+ }
+
+ if (object === null) return;
+
+ var layer = app.scene.mapLayers[object.userData.layerId];
+ if (layer === undefined || layer.type == Q3D.LayerType.DEM) return;
+ if (["Icon", "JSON model", "COLLADA model"].indexOf(layer.objType) != -1) return;
+
+ // create a highlight object (if layer type is Point, slightly bigger than the object)
+ // var highlightObject = new Q3D.Group();
+ var s = (layer.type == Q3D.LayerType.Point) ? 1.01 : 1;
+
+ var clone = object.clone();
+ clone.traverse(function (obj) {
+ obj.material = app.highlightMaterial;
+ });
+ if (s != 1) clone.scale.set(clone.scale.x * s, clone.scale.y * s, clone.scale.z * s);
+ // highlightObject.add(clone);
+
+ // add the highlight object to the scene
+ app.scene.add(clone);
+
+ app.selectedObject = object;
+ app.highlightObject = clone;
+ };
+
+ app.canvasClicked = function (e) {
+ var canvasOffset = app._offset(app.renderer.domElement);
+ var objs = app.intersectObjects(e.clientX - canvasOffset.left, e.clientY - canvasOffset.top);
+ var obj, pt;
+
+ for (var i = 0, l = objs.length; i < l; i++) {
+ obj = objs[i];
+
+ // query marker
+ pt = {x: obj.point.x, y: -obj.point.z, z: obj.point.y}; // obj's coordinate system is y-up
+ app.queryMarker.position.set(pt.x, pt.y, pt.z); // this is z-up
+ app.scene.add(app.queryMarker);
+
+ // get layerId of clicked object
+ var layerId, object = obj.object;
+ while (object) {
+ layerId = object.userData.layerId;
+ if (layerId !== undefined) break;
+ object = object.parent;
+ }
+
+ app.highlightFeature(object);
+ app.render();
+ app.showQueryResult(pt, object);
+
+ return;
+ }
+ app.closePopup();
+ };
+
+ app.saveCanvasImage = function (width, height, fill_background, saveImageFunc) {
+ if (fill_background === undefined) fill_background = true;
+
+ // set canvas size
+ var old_size;
+ if (width && height) {
+ old_size = [app.width, app.height];
+ app.setCanvasSize(width, height);
+ }
+
+ // functions
+ var saveBlob = function (blob) {
+ var filename = "image.png";
+
+ // ie
+ if (window.navigator.msSaveBlob !== undefined) {
+ window.navigator.msSaveBlob(blob, filename);
+ app.popup.hide();
+ }
+ else {
+ // create object url
+ if (app._canvasImageUrl) URL.revokeObjectURL(app._canvasImageUrl);
+ app._canvasImageUrl = URL.createObjectURL(blob);
+
+ // display a link to save the image
+ var e = document.createElement("a");
+ e.className = "download-link";
+ e.href = app._canvasImageUrl;
+ e.download = filename;
+ e.innerHTML = "Save";
+ app.popup.show("Click to save the image to a file." + e.outerHTML, "Image is ready");
+ }
+ };
+
+ var saveCanvasImage = saveImageFunc || function (canvas) {
+ if (canvas.toBlob !== undefined) {
+ canvas.toBlob(saveBlob);
+ }
+ else { // !HTMLCanvasElement.prototype.toBlob
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement.toBlob
+ var binStr = atob(canvas.toDataURL("image/png").split(',')[1]),
+ len = binStr.length,
+ arr = new Uint8Array(len);
+
+ for (var i = 0; i < len; i++) {
+ arr[i] = binStr.charCodeAt(i);
+ }
+
+ saveBlob(new Blob([arr], {type: "image/png"}));
+ }
+ };
+
+ var labels = []; // list of [label point, text]
+ if (app.labelVisible) {
+ var rootGroup = app.scene.labelConnectorGroup, connGroup, conn, pt;
+ for (var i = 0; i < rootGroup.children.length; i++) {
+ connGroup = rootGroup.children[i];
+ if (!connGroup.visible) continue;
+ for (var k = 0; k < connGroup.children.length; k++) {
+ conn = connGroup.children[k];
+ pt = conn.geometry.vertices[0];
+ labels.push({pt: new THREE.Vector3(pt.x, pt.z, -pt.y), // in world coordinates
+ text: conn.userData.elem.textContent});
+ }
+ }
+ }
+
+ var renderLabels = function (ctx) {
+ // context settings
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+
+ // get label style from css
+ var elem = document.createElement("div");
+ elem.className = "print-label";
+ document.body.appendChild(elem);
+ var style = document.defaultView.getComputedStyle(elem, ""),
+ color = style.color;
+ ctx.font = style.font;
+ document.body.removeChild(elem);
+
+ var widthHalf = width / 2,
+ heightHalf = height / 2,
+ camera = app.camera,
+ c2t = new THREE.Vector3(),
+ c2l = new THREE.Vector3(),
+ v = new THREE.Vector3();
+
+ camera.getWorldDirection(c2t);
+
+ // make a list of [label index, distance to camera]
+ var idx_dist = [];
+ for (var i = 0, l = labels.length; i < l; i++) {
+ idx_dist.push([i, camera.position.distanceTo(labels[i].pt)]);
+ }
+
+ // sort label indexes in descending order of distances
+ idx_dist.sort(function (a, b) {
+ if (a[1] < b[1]) return 1;
+ if (a[1] > b[1]) return -1;
+ return 0;
+ });
+
+ var label, text, x, y;
+ for (var i = 0, l = idx_dist.length; i < l; i++) {
+ label = labels[idx_dist[i][0]];
+ if (c2l.subVectors(label.pt, camera.position).dot(c2t) > 0) { // label is in front
+ // calculate label position
+ v.copy(label.pt).project(camera);
+ x = (v.x * widthHalf) + widthHalf;
+ y = -(v.y * heightHalf) + heightHalf;
+ if (x < 0 || width <= x || y < 0 || height <= y) continue;
+
+ // outline effect
+ ctx.fillStyle = "#FFF";
+ for (var j = 0; j < 9; j++) {
+ if (j != 4) ctx.fillText(label.text, x + Math.floor(j / 3) - 1, y + j % 3 - 1);
+ }
+
+ ctx.fillStyle = color;
+ ctx.fillText(label.text, x, y);
+ }
+ }
+ };
+
+ var restoreCanvasSize = function () {
+ // restore canvas size
+ if (old_size) app.setCanvasSize(old_size[0], old_size[1]);
+ app.render();
+ };
+
+ // background option
+ if (!fill_background) app.renderer.setClearColor(0, 0);
+
+ // render
+ app.renderer.preserveDrawingBuffer = true;
+ app.renderer.render(app.scene, app.camera);
+
+ // restore clear color
+ var bgcolor = Q3D.Config.bgColor;
+ app.renderer.setClearColor(bgcolor || 0, (bgcolor === null) ? 0 : 1);
+
+ if ((fill_background && bgcolor === null) || labels.length > 0) {
+ var canvas = document.createElement("canvas");
+ canvas.width = width;
+ canvas.height = height;
+
+ var ctx = canvas.getContext("2d");
+ if (fill_background && bgcolor === null) {
+ // render "sky-like" background
+ var grad = ctx.createLinearGradient(0, 0, 0, height);
+ grad.addColorStop(0, "#98c8f6");
+ grad.addColorStop(0.4, "#cbebff");
+ grad.addColorStop(1, "#f0f9ff");
+ ctx.fillStyle = grad;
+ ctx.fillRect(0, 0, width, height);
+ }
+
+ var image = new Image();
+ image.onload = function () {
+ // draw webgl canvas image
+ ctx.drawImage(image, 0, 0, width, height);
+
+ // render labels
+ if (labels.length > 0) renderLabels(ctx);
+
+ // save canvas image
+ saveCanvasImage(canvas);
+ restoreCanvasSize();
+ };
+ image.src = app.renderer.domElement.toDataURL("image/png");
+ }
+ else {
+ // save webgl canvas image
+ saveCanvasImage(app.renderer.domElement);
+ restoreCanvasSize();
+ }
+ };
+})();
+
+
+/*
+Q3D.Material
+*/
+Q3D.Material = function () {
+ this.loaded = false;
+};
+
+Q3D.Material.prototype = {
+
+ constructor: Q3D.Material,
+
+ // callback is called when material has been completely loaded
+ loadJSONObject: function (jsonObject, callback) {
+ this.origProp = jsonObject;
+
+ var m = jsonObject, opt = {}, defer = false;
+
+ if (m.ds && !Q3D.isIE) opt.side = THREE.DoubleSide;
+
+ if (m.flat) opt.flatShading = true;
+
+ // texture
+ if (m.image !== undefined) {
+ var _this = this;
+ if (m.image.url !== undefined) {
+ opt.map = Q3D.application.loadTextureFile(m.image.url, function () {
+ _this._loadCompleted(callback);
+ });
+ defer = true;
+ }
+ else if (m.image.object !== undefined) { // WebKit Bridge
+ opt.map = new THREE.Texture(m.image.object.toImageData());
+ opt.map.needsUpdate = true;
+ }
+ else { // base64
+ var img = new Image();
+ img.onload = function () {
+ opt.map.needsUpdate = true;
+ _this._loadCompleted(callback);
+ };
+ img.src = m.image.base64;
+ opt.map = new THREE.Texture(img);
+ defer = true;
+ }
+ }
+
+ if (m.c !== undefined) opt.color = m.c;
+
+ if (m.o !== undefined && m.o < 1) {
+ opt.opacity = m.o;
+ opt.transparent = true;
+ }
+
+ if (m.t) opt.transparent = true;
+
+ if (m.w) opt.wireframe = true;
+
+ if (m.type == Q3D.MaterialType.MeshLambert) {
+ this.mtl = new THREE.MeshLambertMaterial(opt);
+ }
+ else if (m.type == Q3D.MaterialType.MeshPhong) {
+ this.mtl = new THREE.MeshPhongMaterial(opt);
+ }
+ else if (m.type == Q3D.MaterialType.MeshToon) {
+ this.mtl = new THREE.MeshToonMaterial(opt);
+ }
+ else if (m.type == Q3D.MaterialType.Point) {
+ opt.size = m.s;
+ this.mtl = new THREE.PointsMaterial(opt);
+ }
+ else if (m.type == Q3D.MaterialType.LineBasic) {
+ this.mtl = new THREE.LineBasicMaterial(opt);
+ }
+ else if (m.type == Q3D.MaterialType.LineDashed) {
+ opt.dashSize = Q3D.Config.line.dash.dashSize;
+ opt.gapSize = Q3D.Config.line.dash.gapSize;
+ this.mtl = new THREE.LineDashedMaterial(opt);
+ }
+ else {
+ opt.color = 0xffffff;
+ this.mtl = new THREE.SpriteMaterial(opt);
+ }
+
+ if (!defer) this._loadCompleted(callback);
+ },
+
+ _loadCompleted: function (anotherCallback) {
+ this.loaded = true;
+
+ if (this._callbacks !== undefined) {
+ for (var i = 0; i < this._callbacks.length; i++) {
+ this._callbacks[i]();
+ }
+ this._callbacks = [];
+ }
+
+ if (anotherCallback) anotherCallback();
+ },
+
+ callbackOnLoad: function (callback) {
+ if (this.loaded) return callback();
+
+ if (this._callbacks === undefined) this._callbacks = [];
+ this._callbacks.push(callback);
+ },
+
+ set: function (material) {
+ this.mtl = material;
+ this.origProp = {};
+ },
+
+ type: function () {
+ if (this.mtl instanceof THREE.MeshLambertMaterial) return Q3D.MaterialType.MeshLambert;
+ if (this.mtl instanceof THREE.MeshPhongMaterial) return Q3D.MaterialType.MeshPhong;
+ if (this.mtl instanceof THREE.LineBasicMaterial) return Q3D.MaterialType.LineBasic;
+ if (this.mtl instanceof THREE.LineDashedMaterial) return Q3D.MaterialType.LineDashed;
+ if (this.mtl instanceof THREE.SpriteMaterial) return Q3D.MaterialType.Sprite;
+ if (this.mtl === undefined) return undefined;
+ if (this.mtl === null) return null;
+ return Q3D.MaterialType.Unknown;
+ },
+
+ dispose: function () {
+ if (!this.mtl) return;
+
+ if (this.mtl.map) this.mtl.map.dispose(); // dispose of texture
+ this.mtl.dispose();
+ this.mtl = null;
+ }
+};
+
+/*
+Q3D.Materials
+*/
+Q3D.Materials = function () {
+ this.materials = [];
+};
+
+Q3D.Materials.prototype = Object.create(THREE.EventDispatcher.prototype);
+Q3D.Materials.prototype.constructor = Q3D.Materials;
+
+// material: instance of Q3D.Material object or THREE.Material-based object
+Q3D.Materials.prototype.add = function (material) {
+ if (material instanceof Q3D.Material) this.materials.push(material);
+ else {
+ var mtl = new Q3D.Material();
+ mtl.set(material);
+ this.materials.push(mtl);
+ }
+};
+
+Q3D.Materials.prototype.get = function (index) {
+ return this.materials[index];
+};
+
+Q3D.Materials.prototype.mtl = function (index) {
+ return this.materials[index].mtl;
+};
+
+Q3D.Materials.prototype.loadJSONObject = function (jsonObject) {
+ var _this = this, iterated = false;
+ var callback = function () {
+ if (iterated) _this.dispatchEvent({type: "renderRequest"});
+ };
+
+ for (var i = 0, l = jsonObject.length; i < l; i++) {
+ var mtl = new Q3D.Material();
+ mtl.loadJSONObject(jsonObject[i], callback);
+ this.add(mtl);
+ }
+ iterated = true;
+};
+
+Q3D.Materials.prototype.dispose = function () {
+ for (var i = 0, l = this.materials.length; i < l; i++) {
+ this.materials[i].dispose();
+ }
+ this.materials = [];
+};
+
+Q3D.Materials.prototype.addFromObject3D = function (object) {
+ var _this = this, mtls = [];
+
+ object.traverse(function (obj) {
+ if (obj.material === undefined) return;
+ ((obj.material instanceof Array) ? obj.material : [obj.material]).forEach(function (mtl) {
+ if (mtls.indexOf(mtl) == -1) {
+ mtls.push(mtl);
+ }
+ });
+ });
+
+ var material;
+ for (var i = 0, l = mtls.length; i < l; i++) {
+ material = new Q3D.Material();
+ material.set(mtls[i]);
+ this.materials.push(material);
+ }
+};
+
+// opacity
+Q3D.Materials.prototype.opacity = function () {
+ if (this.materials.length == 0) return 1;
+
+ var sum = 0;
+ for (var i = 0, l = this.materials.length; i < l; i++) {
+ sum += this.materials[i].mtl.opacity;
+ }
+ return sum / this.materials.length;
+};
+
+Q3D.Materials.prototype.setOpacity = function (opacity) {
+ var material;
+ for (var i = 0, l = this.materials.length; i < l; i++) {
+ material = this.materials[i];
+ material.mtl.transparent = Boolean(material.origProp.t) || (opacity < 1);
+ material.mtl.opacity = opacity;
+ }
+};
+
+// wireframe: boolean
+Q3D.Materials.prototype.setWireframeMode = function (wireframe) {
+ var material;
+ for (var i = 0, l = this.materials.length; i < l; i++) {
+ material = this.materials[i];
+ if (material.origProp.w || material.mtl instanceof THREE.LineBasicMaterial) continue;
+ material.mtl.wireframe = wireframe;
+ }
+};
+
+
+/*
+Q3D.DEMBlock
+*/
+Q3D.DEMBlock = function () {};
+
+Q3D.DEMBlock.prototype = {
+
+ constructor: Q3D.DEMBlock,
+
+ // obj: json object
+ loadJSONObject: function (obj, layer, callback) {
+ var _this = this,
+ grid = obj.grid;
+ this.data = obj;
+
+ // load material
+ this.material = new Q3D.Material();
+ this.material.loadJSONObject(obj.material, function () {
+ layer.requestRender();
+ });
+ layer.materials.add(this.material);
+
+ // create a plane geometry
+ var geom;
+ if (layer.geometryCache) {
+ var params = layer.geometryCache.parameters || {};
+ if (params.width === obj.width && params.height === obj.height &&
+ params.widthSegments === grid.width - 1 && params.heightSegments === grid.height - 1) {
+ geom = layer.geometryCache.clone();
+ geom.parameters = layer.geometryCache.parameters;
+ }
+ }
+ geom = geom || new THREE.PlaneBufferGeometry(obj.width, obj.height, grid.width - 1, grid.height - 1);
+ layer.geometryCache = geom;
+
+ // create a mesh
+ var mesh = new THREE.Mesh(geom, this.material.mtl);
+ mesh.position.fromArray(obj.translate);
+ mesh.scale.z = obj.zScale;
+ layer.addObject(mesh);
+
+ var buildGeometry = function (grid_values) {
+ var vertices = geom.attributes.position.array;
+ for (var i = 0, j = 0, l = vertices.length; i < l; i++, j += 3) {
+ vertices[j + 2] = grid_values[i];
+ }
+ geom.attributes.position.needsUpdate = true;
+
+ if (layer.properties.shading) {
+ geom.computeVertexNormals();
+ }
+
+ if (callback) callback(mesh);
+ };
+
+ if (grid.url !== undefined) {
+ Q3D.application.loadFile(grid.url, "arraybuffer", function (buf) {
+ grid.array = new Float32Array(buf);
+ buildGeometry(grid.array);
+ });
+ }
+ else { // WebKit Bridge
+ if (grid.binary !== undefined) grid.array = new Float32Array(grid.binary.buffer, 0, grid.width * grid.height);
+ buildGeometry(grid.array);
+ }
+
+ this.obj = mesh;
+ return mesh;
+ },
+
+ buildSides: function (layer, parent, material, z0) {
+ var planeWidth = this.data.width,
+ planeHeight = this.data.height,
+ grid = this.data.grid,
+ grid_values = grid.array,
+ w = grid.width,
+ h = grid.height,
+ k = w * (h - 1);
+
+ var e0 = z0 / this.data.zScale - this.data.zShift - (this.data.zShiftA || 0),
+ band_width = -2 * e0;
+
+ // front and back
+ var geom_fr = new THREE.PlaneBufferGeometry(planeWidth, band_width, w - 1, 1),
+ geom_ba = geom_fr.clone();
+
+ var vertices_fr = geom_fr.attributes.position.array,
+ vertices_ba = geom_ba.attributes.position.array;
+
+ var i, mesh;
+ for (i = 0; i < w; i++) {
+ vertices_fr[i * 3 + 1] = grid_values[k + i];
+ vertices_ba[i * 3 + 1] = grid_values[w - 1 - i];
+ }
+ mesh = new THREE.Mesh(geom_fr, material);
+ mesh.rotation.x = Math.PI / 2;
+ mesh.position.y = -planeHeight / 2;
+ mesh.name = "side";
+ parent.add(mesh);
+
+ mesh = new THREE.Mesh(geom_ba, material);
+ mesh.rotation.x = Math.PI / 2;
+ mesh.rotation.y = Math.PI;
+ mesh.position.y = planeHeight / 2;
+ mesh.name = "side";
+ parent.add(mesh);
+
+ // left and right
+ var geom_le = new THREE.PlaneBufferGeometry(band_width, planeHeight, 1, h - 1),
+ geom_ri = geom_le.clone();
+
+ var vertices_le = geom_le.attributes.position.array,
+ vertices_ri = geom_ri.attributes.position.array;
+
+ for (i = 0; i < h; i++) {
+ vertices_le[(i * 2 + 1) * 3] = grid_values[w * i];
+ vertices_ri[i * 2 * 3] = -grid_values[w * (i + 1) - 1];
+ }
+ mesh = new THREE.Mesh(geom_le, material);
+ mesh.rotation.y = -Math.PI / 2;
+ mesh.position.x = -planeWidth / 2;
+ mesh.name = "side";
+ parent.add(mesh);
+
+ mesh = new THREE.Mesh(geom_ri, material);
+ mesh.rotation.y = Math.PI / 2;
+ mesh.position.x = planeWidth / 2;
+ mesh.name = "side";
+ parent.add(mesh);
+
+ // bottom
+ var geom;
+ if (Q3D.Config.exportMode) {
+ geom = new THREE.PlaneBufferGeometry(planeWidth, planeHeight, w - 1, h - 1);
+ }
+ else {
+ geom = new THREE.PlaneBufferGeometry(planeWidth, planeHeight, 1, 1);
+ }
+ mesh = new THREE.Mesh(geom, material);
+ mesh.rotation.x = Math.PI;
+ mesh.position.z = e0;
+ mesh.name = "bottom";
+ parent.add(mesh);
+
+ parent.updateMatrixWorld();
+ },
+
+ buildFrame: function (layer, parent, material, z0) {
+ var grid = this.data.grid,
+ planeWidth = this.data.width,
+ planeHeight = this.data.height;
+
+ // horizontal rectangle at bottom
+ var hw = planeWidth / 2,
+ hh = planeHeight / 2,
+ e0 = z0 / this.data.zScale - this.data.zShift - (this.data.zShiftA || 0);
+ var v = [-hw, -hh, e0,
+ hw, -hh, e0,
+ hw, hh, e0,
+ -hw, hh, e0,
+ -hw, -hh, e0];
+
+ var geom = new THREE.BufferGeometry();
+ geom.addAttribute("position", new THREE.Float32BufferAttribute(v, 3));
+
+ var obj = new THREE.Line(geom, material);
+ obj.name = "frame";
+ parent.add(obj);
+
+ // vertical lines at corners
+ v = [[-hw, -hh, grid.array[grid.array.length - grid.width]],
+ [ hw, -hh, grid.array[grid.array.length - 1]],
+ [ hw, hh, grid.array[grid.width - 1]],
+ [-hw, hh, grid.array[0]]];
+
+ v.forEach(function (p) {
+ var geom = new THREE.BufferGeometry(),
+ vl = [p[0], p[1], p[2], p[0], p[1], e0];
+ geom.addAttribute("position", new THREE.Float32BufferAttribute(vl, 3));
+
+ var obj = new THREE.Line(geom, material);
+ obj.name = "frame";
+ parent.add(obj);
+ });
+
+ parent.updateMatrixWorld();
+ },
+
+ getValue: function (x, y) {
+ var grid = this.data.grid;
+ if (0 <= x && x < grid.width && 0 <= y && y < grid.height) return grid.array[x + grid.width * y];
+ return null;
+ },
+
+ contains: function (x, y) {
+ var translate = this.data.translate,
+ xmin = translate[0] - this.data.width / 2,
+ xmax = translate[0] + this.data.width / 2,
+ ymin = translate[1] - this.data.height / 2,
+ ymax = translate[1] + this.data.height / 2;
+ if (xmin <= x && x <= xmax && ymin <= y && y <= ymax) return true;
+ return false;
+ }
+
+};
+
+
+/*
+Q3D.ClippedDEMBlock
+*/
+Q3D.ClippedDEMBlock = function () {};
+
+Q3D.ClippedDEMBlock.prototype = {
+
+ constructor: Q3D.ClippedDEMBlock,
+
+ loadJSONObject: function (obj, layer, callback) {
+ this.data = obj;
+ var _this = this;
+
+ // load material
+ this.material = new Q3D.Material();
+ this.material.loadJSONObject(obj.material, function () {
+ layer.requestRender();
+ });
+ layer.materials.add(this.material);
+
+ var geom = new THREE.BufferGeometry(),
+ mesh = new THREE.Mesh(geom, this.material.mtl);
+ mesh.position.fromArray(obj.translate);
+ mesh.scale.z = obj.zScale;
+ layer.addObject(mesh);
+
+ var buildGeometry = function (obj) {
+
+ var vertices = obj.triangles.v,
+ base_width = layer.sceneData.width,
+ base_height = layer.sceneData.height;
+ var normals = [], uvs = [];
+ for (var i = 0, l = vertices.length; i < l; i += 3) {
+ normals.push(0, 0, 1);
+ uvs.push(vertices[i] / base_width + 0.5, vertices[i + 1] / base_height + 0.5);
+ }
+
+ geom.setIndex(obj.triangles.f);
+ geom.addAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
+ geom.addAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
+ geom.addAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
+
+ if (layer.properties.shading) {
+ geom.computeVertexNormals();
+ }
+
+ geom.attributes.position.needsUpdate = true;
+ geom.attributes.normal.needsUpdate = true;
+ geom.attributes.uv.needsUpdate = true;
+
+ _this.data.polygons = obj.polygons;
+ if (callback) callback(mesh);
+ };
+
+ if (obj.geom.url !== undefined) {
+ Q3D.application.loadFile(obj.geom.url, "json", function (obj) {
+ buildGeometry(obj);
+ });
+ }
+ else { // WebKit Bridge
+ buildGeometry(obj.geom);
+ }
+
+ this.obj = mesh;
+ return mesh;
+ },
+
+ buildSides: function (layer, parent, material, z0) {
+ var polygons = this.data.polygons,
+ e0 = z0 / this.data.zScale - this.data.zShift - (this.data.zShiftA || 0),
+ bzFunc = function (x, y) { return e0; };
+
+ // make back-side material for bottom
+ var mat_back = material.clone();
+ mat_back.side = THREE.BackSide;
+ layer.materials.add(mat_back);
+
+ var geom, mesh, shape, vertices;
+ for (var i = 0, l = polygons.length; i < l; i++) {
+ var bnds = polygons[i];
+
+ // sides
+ for (var j = 0, m = bnds.length; j < m; j++) {
+ vertices = Q3D.Utils.arrayToVec3Array(bnds[j]);
+ geom = Q3D.Utils.createWallGeometry(vertices, bzFunc, true);
+ mesh = new THREE.Mesh(geom, material);
+ mesh.name = "side";
+ parent.add(mesh);
+ }
+
+ // bottom
+ shape = new THREE.Shape(Q3D.Utils.arrayToVec2Array(bnds[0]));
+ for (var j = 1, m = bnds.length; j < m; j++) {
+ shape.holes.push(new THREE.Path(Q3D.Utils.arrayToVec2Array(bnds[j])));
+ }
+ geom = new THREE.ShapeBufferGeometry(shape);
+ mesh = new THREE.Mesh(geom, mat_back);
+ mesh.position.z = e0;
+ mesh.name = "bottom";
+ parent.add(mesh);
+ }
+ parent.updateMatrixWorld();
+ },
+
+ // not implemented
+ getValue: function (x, y) {
+ return null;
+ },
+
+ // not implemented
+ contains: function (x, y) {
+ return false;
+ }
+
+};
+
+/*
+Q3D.MapLayer
+*/
+Q3D.MapLayer = function () {
+ this.properties = {};
+ this.queryable = true;
+
+ this.materials = new Q3D.Materials();
+ this.materials.addEventListener("renderRequest", this.requestRender.bind(this));
+
+ this.objectGroup = new Q3D.Group();
+ this.queryObjs = [];
+};
+
+Q3D.MapLayer.prototype = Object.create(THREE.EventDispatcher.prototype);
+Q3D.MapLayer.prototype.constructor = Q3D.MapLayer;
+
+Q3D.MapLayer.prototype.addObject = function (object, queryable) {
+ if (queryable === undefined) queryable = this.queryable;
+
+ object.userData.layerId = this.id;
+ this.objectGroup.add(object);
+
+ if (queryable) {
+ var queryObjs = this.queryObjs;
+ object.traverse(function (obj) {
+ queryObjs.push(obj);
+ });
+ }
+ return this.objectGroup.children.length - 1;
+};
+
+Q3D.MapLayer.prototype.queryableObjects = function () {
+ return (this.visible) ? this.queryObjs : [];
+};
+
+Q3D.MapLayer.prototype.removeAllObjects = function () {
+ // dispose of geometries
+ this.objectGroup.traverse(function (obj) {
+ if (obj.geometry) obj.geometry.dispose();
+ });
+
+ // dispose of materials
+ this.materials.dispose();
+
+ // remove all child objects from object group
+ for (var i = this.objectGroup.children.length - 1 ; i >= 0; i--) {
+ this.objectGroup.remove(this.objectGroup.children[i]);
+ }
+ this.queryObjs = [];
+};
+
+Q3D.MapLayer.prototype.loadJSONObject = function (jsonObject, scene) {
+ if (jsonObject.type == "layer") {
+ // properties
+ if (jsonObject.properties !== undefined) {
+ this.properties = jsonObject.properties;
+ this.visible = (jsonObject.properties.visible || Q3D.Config.allVisible) ? true : false;
+ }
+
+ if (jsonObject.data !== undefined) {
+ this.removeAllObjects();
+
+ // materials
+ if (jsonObject.data.materials !== undefined) {
+ this.materials.loadJSONObject(jsonObject.data.materials);
+ }
+ }
+
+ this.sceneData = scene.userData;
+ this._bbox = undefined;
+ }
+};
+
+Object.defineProperty(Q3D.MapLayer.prototype, "opacity", {
+ get: function () {
+ return this.materials.opacity();
+ },
+ set: function (value) {
+ this.materials.setOpacity(value);
+ this.requestRender();
+ }
+});
+
+Object.defineProperty(Q3D.MapLayer.prototype, "visible", {
+ get: function () {
+ return this.objectGroup.visible;
+ },
+ set: function (value) {
+ this.objectGroup.visible = value;
+ this.requestRender();
+ }
+});
+
+Q3D.MapLayer.prototype.boundingBox = function (forceUpdate) {
+ if (!this._bbox || forceUpdate) {
+ this._bbox = new THREE.Box3().setFromObject(this.objectGroup);
+ }
+ return this._bbox;
+}
+
+Q3D.MapLayer.prototype.setWireframeMode = function (wireframe) {
+ this.materials.setWireframeMode(wireframe);
+};
+
+Q3D.MapLayer.prototype.requestRender = function () {
+ this.dispatchEvent({type: "renderRequest"});
+};
+
+
+/*
+Q3D.DEMLayer --> Q3D.MapLayer
+*/
+Q3D.DEMLayer = function () {
+ Q3D.MapLayer.call(this);
+ this.type = Q3D.LayerType.DEM;
+ this.blocks = [];
+};
+
+Q3D.DEMLayer.prototype = Object.create(Q3D.MapLayer.prototype);
+Q3D.DEMLayer.prototype.constructor = Q3D.DEMLayer;
+
+Q3D.DEMLayer.prototype.loadJSONObject = function (jsonObject, scene) {
+ var old_shading = this.properties.shading;
+ Q3D.MapLayer.prototype.loadJSONObject.call(this, jsonObject, scene);
+ if (jsonObject.type == "layer") {
+ if (old_shading != jsonObject.properties.shading) {
+ this.geometryCache = null;
+ }
+
+ if (jsonObject.data !== undefined) {
+ jsonObject.data.forEach(function (obj) {
+ this.buildBlock(obj, scene);
+ }, this);
+ }
+ }
+ else if (jsonObject.type == "block") {
+ this.buildBlock(jsonObject, scene);
+ }
+};
+
+Q3D.DEMLayer.prototype.buildBlock = function (jsonObject, scene) {
+ var _this = this;
+
+ var index = jsonObject.block;
+ this.blocks[index] = (jsonObject.grid !== undefined) ? (new Q3D.DEMBlock()) : (new Q3D.ClippedDEMBlock());
+ this.blocks[index].loadJSONObject(jsonObject, this, function (m) {
+
+ if (jsonObject.sides || jsonObject.frame) {
+
+ _this.sideVisible = true;
+
+ var buildSides = function () {
+ var material;
+ // build sides and bottom
+ if (jsonObject.sides) {
+ material = new Q3D.Material();
+ material.loadJSONObject(jsonObject.sides.mtl);
+ _this.materials.add(material);
+
+ _this.blocks[index].buildSides(_this, m, material.mtl, Q3D.Config.dem.side.bottomZ);
+ }
+ // build frame
+ if (jsonObject.frame) {
+ material = new Q3D.Material();
+ material.loadJSONObject(jsonObject.frame.mtl);
+ _this.materials.add(material);
+
+ _this.blocks[index].buildFrame(_this, m, material.mtl, Q3D.Config.dem.frame.bottomZ);
+ }
+ _this.requestRender();
+ };
+
+ if (Q3D.Config.autoZShift) {
+ scene.addEventListener("zShiftAdjusted", function listener(event) {
+ // set adjusted z shift to every block
+ _this.blocks.forEach(function (block) {
+ block.data.zShiftA = event.sceneData.zShiftA;
+ });
+ buildSides();
+ scene.removeEventListener("zShiftAdjusted", listener);
+ });
+ }
+ else {
+ buildSides();
+ }
+ }
+ _this.requestRender();
+ });
+};
+
+// calculate elevation at the coordinates (x, y) on triangle face
+Q3D.DEMLayer.prototype.getZ = function (x, y) {
+ for (var i = 0, l = this.blocks.length; i < l; i++) {
+ var block = this.blocks[i],
+ data = block.data;
+ if (!block.contains(x, y)) continue;
+
+ var ix = data.width / (data.grid.width - 1),
+ iy = data.height / (data.grid.height - 1);
+
+ var xmin = data.translate[0] - data.width / 2,
+ ymax = data.translate[1] + data.height / 2;
+
+ var mx0 = Math.floor((x - xmin) / ix),
+ my0 = Math.floor((ymax - y) / iy);
+
+ var z = [block.getValue(mx0, my0),
+ block.getValue(mx0 + 1, my0),
+ block.getValue(mx0, my0 + 1),
+ block.getValue(mx0 + 1, my0 + 1)];
+
+ var px0 = xmin + ix * mx0,
+ py0 = ymax - iy * my0;
+
+ var sdx = (x - px0) / ix,
+ sdy = (py0 - y) / iy;
+
+ // console.log(x, y, mx0, my0, sdx, sdy);
+
+ if (sdx <= 1 - sdy) return z[0] + (z[1] - z[0]) * sdx + (z[2] - z[0]) * sdy;
+ else return z[3] + (z[2] - z[3]) * (1 - sdx) + (z[1] - z[3]) * (1 - sdy);
+ }
+ return null;
+};
+
+Q3D.DEMLayer.prototype.segmentizeLineString = function (lineString, zFunc) {
+ // does not support multiple blocks
+ if (zFunc === undefined) zFunc = function () { return 0; };
+ var width = this.sceneData.width,
+ height = this.sceneData.height;
+ var xmin = -width / 2,
+ ymax = height / 2;
+ var grid = this.blocks[0].data.grid,
+ ix = width / (grid.width - 1),
+ iy = height / (grid.height - 1);
+ var sort_func = function (a, b) { return a - b; };
+
+ var pts = [];
+ for (var i = 1, l = lineString.length; i < l; i++) {
+ var pt1 = lineString[i - 1], pt2 = lineString[i];
+ var x1 = pt1[0], x2 = pt2[0], y1 = pt1[1], y2 = pt2[1], z1 = pt1[2], z2 = pt2[2];
+ var nx1 = (x1 - xmin) / ix,
+ nx2 = (x2 - xmin) / ix;
+ var ny1 = (ymax - y1) / iy,
+ ny2 = (ymax - y2) / iy;
+ var ns1 = Math.abs(ny1 + nx1),
+ ns2 = Math.abs(ny2 + nx2);
+
+ var p = [0], nvp = [[nx1, nx2], [ny1, ny2], [ns1, ns2]];
+ for (var j = 0; j < 3; j++) {
+ var v1 = nvp[j][0], v2 = nvp[j][1];
+ if (v1 == v2) continue;
+ var k = Math.ceil(Math.min(v1, v2));
+ var n = Math.floor(Math.max(v1, v2));
+ for (; k <= n; k++) {
+ p.push((k - v1) / (v2 - v1));
+ }
+ }
+
+ p.sort(sort_func);
+
+ var x, y, z, lp = null;
+ for (var j = 0, m = p.length; j < m; j++) {
+ if (lp === p[j]) continue;
+ if (p[j] == 1) break;
+
+ x = x1 + (x2 - x1) * p[j];
+ y = y1 + (y2 - y1) * p[j];
+
+ if (z1 === undefined || z2 === undefined) z = zFunc(x, y);
+ else z = z1 + (z2 - z1) * p[j];
+
+ pts.push(new THREE.Vector3(x, y, z));
+
+ // Q3D.Utils.putStick(x, y, zFunc);
+
+ lp = p[j];
+ }
+ }
+ // last point (= the first point)
+ var pt = lineString[lineString.length - 1];
+ pts.push(new THREE.Vector3(pt[0], pt[1], (pt[2] === undefined) ? zFunc(pt[0], pt[1]) : pt[2]));
+
+ /*
+ for (var i = 0, l = lineString.length - 1; i < l; i++) {
+ Q3D.Utils.putStick(lineString[i][0], lineString[i][1], zFunc, 0.8);
+ }
+ */
+
+ return pts;
+};
+
+Q3D.DEMLayer.prototype.setSideVisible = function (visible) {
+ this.sideVisible = visible;
+ this.objectGroup.traverse(function (obj) {
+ if (obj.name == "side" || obj.name == "bottom" || obj.name == "frame") obj.visible = visible;
+ });
+};
+
+
+/*
+Q3D.VectorLayer --> Q3D.MapLayer
+*/
+Q3D.VectorLayer = function () {
+ Q3D.MapLayer.call(this);
+
+ // this.labelConnectorGroup = undefined;
+ // this.labelParentElement = undefined;
+};
+
+Q3D.VectorLayer.prototype = Object.create(Q3D.MapLayer.prototype);
+Q3D.VectorLayer.prototype.constructor = Q3D.VectorLayer;
+
+Q3D.VectorLayer.prototype.build = function (block) {};
+
+Q3D.VectorLayer.prototype.clearLabels = function () {
+ if (this.labelConnectorGroup) this.labelConnectorGroup.clear();
+
+ // create parent element for labels
+ var elem = this.labelParentElement;
+ if (elem) {
+ while (elem.lastChild) {
+ elem.removeChild(elem.lastChild);
+ }
+ }
+};
+
+Q3D.VectorLayer.prototype.buildLabels = function (features, getPointsFunc) {
+ if (this.properties.label === undefined || getPointsFunc === undefined) return;
+
+ var zShift = this.sceneData.zShift,
+ zScale = this.sceneData.zScale,
+ z0 = zShift * zScale;
+ var prop = this.properties.label,
+ pIndex = prop.index,
+ isRelative = prop.relative;
+
+ var line_mat = new THREE.LineBasicMaterial({color: Q3D.Config.label.connectorColor});
+ var f, text, e, pt0, pt1, geom, conn;
+
+ for (var i = 0, l = features.length; i < l; i++) {
+ f = features[i];
+ text = f.prop[pIndex];
+ if (text === null || text === "") continue;
+
+ getPointsFunc(f).forEach(function (pt) {
+ // create div element for label
+ e = document.createElement("div");
+ e.className = "label";
+ e.innerHTML = text;
+ this.labelParentElement.appendChild(e);
+
+ pt0 = new THREE.Vector3(pt[0], pt[1], pt[2]); // bottom
+ pt1 = new THREE.Vector3(pt[0], pt[1], (isRelative) ? pt[2] + f.lh : z0 + f.lh); // top
+
+ if (Q3D.Config.label.queryable) {
+ var obj = this.objectGroup.children[f.objIndices[0]];
+ e.onclick = function () {
+ app.scene.remove(app.queryMarker);
+ app.highlightFeature(obj);
+ app.render();
+ app.showQueryResult({x: pt[0], y: pt[1], z: pt[2]}, obj, true);
+ };
+ e.classList.add("queryable");
+ }
+ else {
+ e.classList.add("no-events");
+ }
+
+ // create connector
+ geom = new THREE.Geometry();
+ geom.vertices.push(pt1, pt0);
+
+ conn = new THREE.Line(geom, line_mat);
+ conn.userData.layerId = this.id;
+ //conn.userData.featureId = i;
+ conn.userData.elem = e;
+
+ this.labelConnectorGroup.add(conn);
+
+ }, this);
+ }
+};
+
+Q3D.VectorLayer.prototype.loadJSONObject = function (jsonObject, scene) {
+ Q3D.MapLayer.prototype.loadJSONObject.call(this, jsonObject, scene);
+ if (jsonObject.type == "layer") {
+ if (jsonObject.data !== undefined) {
+ this.clearLabels();
+
+ // build labels
+ if (this.properties.label !== undefined) {
+ // create a label connector group
+ if (this.labelConnectorGroup === undefined) {
+ this.labelConnectorGroup = new Q3D.Group();
+ this.labelConnectorGroup.userData.layerId = this.id;
+ this.labelConnectorGroup.visible = this.visible;
+ scene.labelConnectorGroup.add(this.labelConnectorGroup);
+ }
+
+ // create a label parent element
+ if (this.labelParentElement === undefined) {
+ this.labelParentElement = document.createElement("div");
+ this.labelParentElement.style.display = (this.visible) ? "block" : "none";
+ scene.labelRootElement.appendChild(this.labelParentElement);
+ }
+ }
+
+ (jsonObject.data.blocks || []).forEach(function (block) {
+ if (block.url !== undefined) Q3D.application.loadJSONFile(block.url);
+ else {
+ this.build(block.features);
+ if (this.properties.label !== undefined) this.buildLabels(block.features);
+ }
+ }, this);
+ }
+ }
+ else if (jsonObject.type == "block") {
+ this.build(jsonObject.features);
+ if (this.properties.label !== undefined) this.buildLabels(jsonObject.features);
+ }
+};
+
+Object.defineProperty(Q3D.VectorLayer.prototype, "visible", {
+ get: function () {
+ return Object.getOwnPropertyDescriptor(Q3D.MapLayer.prototype, "visible").get.call(this);
+ },
+ set: function (value) {
+ if (this.labelParentElement) this.labelParentElement.style.display = (value) ? "block" : "none";
+ if (this.labelConnectorGroup) this.labelConnectorGroup.visible = value;
+ Object.getOwnPropertyDescriptor(Q3D.MapLayer.prototype, "visible").set.call(this, value);
+ }
+});
+
+
+/*
+Q3D.PointLayer --> Q3D.VectorLayer
+*/
+Q3D.PointLayer = function () {
+ Q3D.VectorLayer.call(this);
+ this.type = Q3D.LayerType.Point;
+};
+
+Q3D.PointLayer.prototype = Object.create(Q3D.VectorLayer.prototype);
+Q3D.PointLayer.prototype.constructor = Q3D.PointLayer;
+
+Q3D.PointLayer.prototype.loadJSONObject = function (jsonObject, scene) {
+ Q3D.VectorLayer.prototype.loadJSONObject.call(this, jsonObject, scene);
+ if (jsonObject.type == "layer" && jsonObject.properties.objType == "Model File" && jsonObject.data !== undefined) {
+ if (this.models === undefined) {
+ var _this = this;
+
+ this.models = new Q3D.Models();
+ this.models.addEventListener("modelLoaded", function (event) {
+ _this.materials.addFromObject3D(event.model.scene);
+ _this.requestRender();
+ });
+ }
+ else {
+ this.models.clear();
+ }
+ this.models.loadJSONObject(jsonObject.data.models);
+ }
+};
+
+Q3D.PointLayer.prototype.build = function (features) {
+ var objType = this.properties.objType;
+ if (objType == "Point") { this.buildPoints(features); return; }
+ if (objType == "Icon") { this.buildIcons(features); return; }
+ if (objType == "Model File") { this.buildModels(features); return; }
+
+ var deg2rad = Math.PI / 180, rx = 90 * deg2rad;
+ var setSR, unitGeom;
+
+ if (this.cachedGeometryType == objType) {
+ unitGeom = this.geometryCache;
+ }
+
+ if (objType == "Sphere") {
+ setSR = function (mesh, geom) {
+ mesh.scale.set(geom.r, geom.r, geom.r);
+ };
+ unitGeom = unitGeom || new THREE.SphereBufferGeometry(1, 32, 32);
+ }
+ else if (objType == "Box") {
+ setSR = function (mesh, geom) {
+ mesh.scale.set(geom.w, geom.h, geom.d);
+ mesh.rotation.x = rx;
+ };
+ unitGeom = unitGeom || new THREE.BoxBufferGeometry(1, 1, 1);
+ }
+ else if (objType == "Disk") {
+ var sz = this.sceneData.zExaggeration; // set 1 if not to be elongated
+ setSR = function (mesh, geom) {
+ mesh.scale.set(geom.r, geom.r * sz, 1);
+ mesh.rotateOnWorldAxis(Q3D.uv.i, -geom.d * deg2rad);
+ mesh.rotateOnWorldAxis(Q3D.uv.k, -geom.dd * deg2rad);
+ };
+ unitGeom = unitGeom || new THREE.CircleBufferGeometry(1, 32);
+ }
+ else if (objType == "Plane") {
+ var sz = this.sceneData.zExaggeration; // set 1 if not to be elongated
+ setSR = function (mesh, geom) {
+ mesh.scale.set(geom.w, geom.l * sz, 1);
+ mesh.rotateOnWorldAxis(Q3D.uv.i, -geom.d * deg2rad);
+ mesh.rotateOnWorldAxis(Q3D.uv.k, -geom.dd * deg2rad);
+ };
+ unitGeom = unitGeom || new THREE.PlaneBufferGeometry(1, 1, 1, 1);
+ }
+ else { // Cylinder or Cone
+ setSR = function (mesh, geom) {
+ mesh.scale.set(geom.r, geom.h, geom.r);
+ mesh.rotation.x = rx;
+ };
+ unitGeom = unitGeom || ((objType == "Cylinder") ? new THREE.CylinderBufferGeometry(1, 1, 1, 32) : new THREE.CylinderBufferGeometry(0, 1, 1, 32));
+ }
+
+ // iteration for features
+ var materials = this.materials;
+ var f, geom, z_addend, i, l, mesh, pt;
+ for (var fidx = 0, flen = features.length; fidx < flen; fidx++) {
+ f = features[fidx];
+ f.objIndices = []
+
+ geom = f.geom;
+ z_addend = (geom.h) ? geom.h / 2 : 0;
+ for (i = 0, l = geom.pts.length; i < l; i++) {
+ mesh = new THREE.Mesh(unitGeom, materials.mtl(f.mtl));
+ setSR(mesh, geom);
+
+ pt = geom.pts[i];
+ mesh.position.set(pt[0], pt[1], pt[2] + z_addend);
+ mesh.userData.properties = f.prop;
+
+ f.objIndices.push(this.addObject(mesh));
+ }
+ }
+
+ this.geometryCache = unitGeom;
+ this.cachedGeometryType = objType;
+};
+
+Q3D.PointLayer.prototype.buildPoints = function (features) {
+ var f, geom, obj;
+ for (var fidx = 0, flen = features.length; fidx < flen; fidx++) {
+ f = features[fidx];
+
+ geom = new THREE.BufferGeometry();
+ geom.addAttribute("position",
+ new THREE.BufferAttribute(new Float32Array(f.geom.pts), 3));
+
+ obj = new THREE.Points(geom, this.materials.mtl(f.mtl));
+ obj.userData.properties = f.prop;
+
+ f.objIndices = [this.addObject(obj)];
+ }
+};
+
+Q3D.PointLayer.prototype.buildIcons = function (features) {
+ // each feature in this layer
+ features.forEach(function (f) {
+ var sprite,
+ objs = [],
+ material = this.materials.get(f.mtl);
+
+ f.objIndices = [];
+ for (var i = 0, l = f.geom.pts.length; i < l; i++) {
+ sprite = new THREE.Sprite(material.mtl);
+ sprite.position.fromArray(f.geom.pts[i]);
+ sprite.userData.properties = f.prop;
+
+ objs.push(sprite);
+ f.objIndices.push(this.addObject(sprite));
+ }
+
+ material.callbackOnLoad(function () {
+ var img = material.mtl.map.image;
+ for (var i = 0; i < objs.length; i++) {
+ // base size is 64 x 64
+ objs[i].scale.set(img.width / 64 * f.geom.scale,
+ img.height / 64 * f.geom.scale,
+ 1);
+ objs[i].updateMatrixWorld();
+ }
+ });
+ }, this);
+};
+
+Q3D.PointLayer.prototype.buildModels = function (features) {
+ var _this = this,
+ q = new THREE.Quaternion(),
+ e = new THREE.Euler(),
+ deg2rad = Math.PI / 180;
+
+ // each feature in this layer
+ features.forEach(function (f) {
+ var model = _this.models.get(f.model);
+ if (model === undefined) {
+ console.log("Model File: There is a missing model.");
+ return;
+ }
+
+ f.objIndices = [];
+ f.geom.pts.forEach(function (pt) {
+ model.callbackOnLoad(function (m) {
+ var obj = m.scene.clone();
+ obj.scale.set(f.geom.scale, f.geom.scale, f.geom.scale);
+
+ if (obj.rotation.x) { // == -Math.PI / 2 (z-up model)
+ // reset coordinate system to z-up and specified rotation
+ obj.rotation.set(0, 0, 0);
+ obj.quaternion.multiply(q.setFromEuler(e.set(f.geom.rotateX * deg2rad,
+ f.geom.rotateY * deg2rad,
+ f.geom.rotateZ * deg2rad,
+ f.geom.rotateO || "XYZ")));
+ }
+ else {
+ // y-up to z-up and specified rotation
+ obj.quaternion.multiply(q.setFromEuler(e.set(f.geom.rotateX * deg2rad,
+ f.geom.rotateY * deg2rad,
+ f.geom.rotateZ * deg2rad,
+ f.geom.rotateO || "XYZ")));
+ obj.quaternion.multiply(q.setFromEuler(e.set(Math.PI / 2, 0, 0)));
+ }
+
+ var parent = new THREE.Group();
+ parent.scale.set(1, 1, _this.sceneData.zExaggeration);
+ parent.position.fromArray(pt);
+ parent.userData.properties = f.prop;
+ parent.add(obj);
+
+ f.objIndices.push(_this.addObject(parent));
+ });
+ });
+ });
+};
+
+Q3D.PointLayer.prototype.buildLabels = function (features) {
+ Q3D.VectorLayer.prototype.buildLabels.call(this, features, function (f) { return f.geom.pts; });
+};
+
+
+/*
+Q3D.LineLayer --> Q3D.VectorLayer
+*/
+Q3D.LineLayer = function () {
+ Q3D.VectorLayer.call(this);
+ this.type = Q3D.LayerType.Line;
+};
+
+Q3D.LineLayer.prototype = Object.create(Q3D.VectorLayer.prototype);
+Q3D.LineLayer.prototype.constructor = Q3D.LineLayer;
+
+Q3D.LineLayer.prototype.loadJSONObject = function (jsonObject, scene) {
+ Q3D.VectorLayer.prototype.loadJSONObject.call(this, jsonObject, scene);
+};
+
+Q3D.LineLayer.prototype.build = function (features) {
+ var createObject,
+ objType = this.properties.objType,
+ materials = this.materials,
+ sceneData = this.sceneData;
+
+ if (objType == this._lastObjType && this._createObject !== undefined) {
+ createObject = this._createObject;
+ }
+ else if (objType == "Line") {
+ createObject = function (f, line) {
+ var pt, vertices = [];
+ for (var i = 0, l = line.length; i < l; i++) {
+ pt = line[i];
+ vertices.push(pt[0], pt[1], pt[2]);
+ }
+ var geom = new THREE.BufferGeometry();
+ geom.addAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
+
+ var obj = new THREE.Line(geom, materials.mtl(f.mtl));
+ if (obj.material instanceof THREE.LineDashedMaterial) obj.computeLineDistances();
+ return obj;
+ };
+ }
+ else if (objType == "Pipe" || objType == "Cone") {
+ var jointGeom, cylinGeom;
+ if (objType == "Pipe") {
+ jointGeom = new THREE.SphereBufferGeometry(1, 32, 32);
+ cylinGeom = new THREE.CylinderBufferGeometry(1, 1, 1, 32);
+ }
+ else {
+ cylinGeom = new THREE.CylinderBufferGeometry(0, 1, 1, 32);
+ }
+
+ var mesh, pt0 = new THREE.Vector3(), pt1 = new THREE.Vector3(), sub = new THREE.Vector3(), axis = Q3D.uv.j;
+
+ createObject = function (f, line) {
+ var group = new Q3D.Group();
+
+ pt0.set(line[0][0], line[0][1], line[0][2]);
+ for (var i = 1, l = line.length; i < l; i++) {
+ pt1.set(line[i][0], line[i][1], line[i][2]);
+
+ mesh = new THREE.Mesh(cylinGeom, materials.mtl(f.mtl));
+ mesh.scale.set(f.geom.r, pt0.distanceTo(pt1), f.geom.r);
+ mesh.position.set((pt0.x + pt1.x) / 2, (pt0.y + pt1.y) / 2, (pt0.z + pt1.z) / 2);
+ mesh.quaternion.setFromUnitVectors(axis, sub.subVectors(pt1, pt0).normalize());
+ group.add(mesh);
+
+ if (jointGeom && i < l - 1) {
+ mesh = new THREE.Mesh(jointGeom, materials.mtl(f.mtl));
+ mesh.scale.set(f.geom.r, f.geom.r, f.geom.r);
+ mesh.position.copy(pt1);
+ group.add(mesh);
+ }
+
+ pt0.copy(pt1);
+ }
+ return group;
+ };
+ }
+ else if (objType == "Box") {
+ // In this method, box corners are exposed near joint when both azimuth and slope of
+ // the segments of both sides are different. Also, some unnecessary faces are created.
+ var faces = [], vi;
+ vi = [[0, 5, 4], [4, 5, 1], // left turn - top, side, bottom
+ [3, 0, 7], [7, 0, 4],
+ [6, 3, 2], [2, 3, 7],
+ [4, 1, 0], [0, 1, 5], // right turn - top, side, bottom
+ [1, 2, 5], [5, 2, 6],
+ [2, 7, 6], [6, 7, 3]];
+
+ for (var j = 0; j < 12; j++) {
+ faces.push(new THREE.Face3(vi[j][0], vi[j][1], vi[j][2]));
+ }
+
+ createObject = function (f, line) {
+ var geometry = new THREE.Geometry(),
+ group = new Q3D.Group(); // used in debug mode
+
+ var geom, mesh, dist, quat, rx, rz, wh4, vb4, vf4;
+ var pt0 = new THREE.Vector3(), pt1 = new THREE.Vector3(), sub = new THREE.Vector3(),
+ pt = new THREE.Vector3(), ptM = new THREE.Vector3(), scale1 = new THREE.Vector3(1, 1, 1),
+ matrix = new THREE.Matrix4(), quat = new THREE.Quaternion();
+
+ pt0.set(line[0][0], line[0][1], line[0][2]);
+ for (var i = 1, l = line.length; i < l; i++) {
+ pt1.set(line[i][0], line[i][1], line[i][2]);
+ dist = pt0.distanceTo(pt1);
+ sub.subVectors(pt1, pt0);
+ rx = Math.atan2(sub.z, Math.sqrt(sub.x * sub.x + sub.y * sub.y));
+ rz = Math.atan2(sub.y, sub.x) - Math.PI / 2;
+ ptM.set((pt0.x + pt1.x) / 2, (pt0.y + pt1.y) / 2, (pt0.z + pt1.z) / 2); // midpoint
+ quat.setFromEuler(new THREE.Euler(rx, 0, rz, "ZXY"));
+ matrix.compose(ptM, quat, scale1);
+
+ // place a box to the segment
+ geom = new THREE.BoxGeometry(f.geom.w, dist, f.geom.h);
+ geom.applyMatrix(matrix);
+ geometry.merge(geom);
+
+ // joint
+ // 4 vertices of backward side of current segment
+ wh4 = [[-f.geom.w / 2, f.geom.h / 2],
+ [f.geom.w / 2, f.geom.h / 2],
+ [f.geom.w / 2, -f.geom.h / 2],
+ [-f.geom.w / 2, -f.geom.h / 2]];
+ vb4 = [];
+ for (j = 0; j < 4; j++) {
+ pt.set(wh4[j][0], -dist / 2, wh4[j][1]);
+ pt.applyMatrix4(matrix);
+ vb4.push(pt.clone());
+ }
+
+ if (vf4) {
+ geom = new THREE.Geometry();
+ geom.vertices = vf4.concat(vb4);
+ geom.faces = faces;
+ geometry.merge(geom);
+ }
+
+ // 4 vertices of forward side
+ vf4 = [];
+ for (j = 0; j < 4; j++) {
+ pt.set(wh4[j][0], dist / 2, wh4[j][1]);
+ pt.applyMatrix4(matrix);
+ vf4.push(new THREE.Vector3(pt.x, pt.y, pt.z));
+ }
+
+ pt0.copy(pt1);
+ }
+
+ geometry.faceVertexUvs = [[]];
+ geometry.mergeVertices();
+ geometry.computeFaceNormals();
+ if (Q3D.Config.exportMode) geometry = new THREE.BufferGeometry().fromGeometry(geometry);
+ return new THREE.Mesh(geometry, materials.mtl(f.mtl));
+ };
+ }
+ else if (objType == "Wall") {
+ var z0 = sceneData.zShift * sceneData.zScale;
+
+ createObject = function (f, line) {
+ var pt;
+ var vertices = [];
+ for (var i = 0, l = line.length; i < l; i++) {
+ pt = line[i];
+ vertices.push(new THREE.Vector3(pt[0], pt[1], pt[2]));
+ }
+ var bzFunc = function (x, y) { return z0 + f.geom.bh; };
+ return new THREE.Mesh(Q3D.Utils.createWallGeometry(vertices, bzFunc),
+ materials.mtl(f.mtl));
+ };
+ }
+
+ // each feature in this layer
+ var f, i, l, obj;
+ for (var fidx = 0, flen = features.length; fidx < flen; fidx++) {
+ f = features[fidx];
+ f.objIndices = [];
+
+ for (i = 0, l = f.geom.lines.length; i < l; i++) {
+ obj = createObject(f, f.geom.lines[i]);
+ obj.userData.properties = f.prop;
+
+ f.objIndices.push(this.addObject(obj));
+ }
+ }
+
+ this._lastObjType = objType;
+ this._createObject = createObject;
+};
+
+Q3D.LineLayer.prototype.buildLabels = function (features) {
+ // Line layer doesn't support label
+ // Q3D.VectorLayer.prototype.buildLabels.call(this, features);
+};
+
+
+/*
+Q3D.PolygonLayer --> Q3D.VectorLayer
+*/
+Q3D.PolygonLayer = function () {
+ Q3D.VectorLayer.call(this);
+ this.type = Q3D.LayerType.Polygon;
+
+ // for overlay
+ this.borderVisible = true;
+ this.sideVisible = true;
+};
+
+Q3D.PolygonLayer.prototype = Object.create(Q3D.VectorLayer.prototype);
+Q3D.PolygonLayer.prototype.constructor = Q3D.PolygonLayer;
+
+Q3D.PolygonLayer.prototype.loadJSONObject = function (jsonObject, scene) {
+ Q3D.VectorLayer.prototype.loadJSONObject.call(this, jsonObject, scene);
+};
+
+Q3D.PolygonLayer.prototype.build = function (features) {
+ var createObject,
+ materials = this.materials,
+ sceneData = this.sceneData;
+
+ if (this.properties.objType == this._lastObjType && this._createObject !== undefined) {
+ createObject = this._createObject;
+ }
+ else if (this.properties.objType == "Polygon") {
+ createObject = function (f) {
+ var geom = new THREE.BufferGeometry();
+ geom.addAttribute("position", new THREE.Float32BufferAttribute(f.geom.triangles.v, 3));
+ geom.setIndex(f.geom.triangles.f);
+ if (!Q3D.Config.exportMode) {
+ geom = new THREE.Geometry().fromBufferGeometry(geom); // Flat shading doesn't work with combination of
+ // BufferGeometry and Lambert/Toon material.
+ }
+ return new THREE.Mesh(geom, materials.mtl(f.mtl));
+ };
+ }
+ else if (this.properties.objType == "Extruded") {
+ var createSubObject = function (f, polygon, z) {
+ var i, l, j, m;
+
+ var shape = new THREE.Shape(Q3D.Utils.arrayToVec2Array(polygon[0]));
+ for (i = 1, l = polygon.length; i < l; i++) {
+ shape.holes.push(new THREE.Path(Q3D.Utils.arrayToVec2Array(polygon[i])));
+ }
+
+ // extruded geometry
+ var geom = new THREE.ExtrudeBufferGeometry(shape, {bevelEnabled: false, depth: f.geom.h});
+ var mesh = new THREE.Mesh(geom, materials.mtl(f.mtl.face));
+ mesh.position.z = z;
+
+ if (f.mtl.edge !== undefined) {
+ // edge
+ var edge, bnd, p, v,
+ h = f.geom.h,
+ mtl = materials.mtl(f.mtl.edge);
+
+ for (i = 0, l = polygon.length; i < l; i++) {
+ bnd = polygon[i];
+
+ v = [];
+ for (j = 0, m = bnd.length; j < m; j++) {
+ v.push(bnd[j][0], bnd[j][1], 0);
+ }
+
+ geom = new THREE.BufferGeometry();
+ geom.addAttribute("position", new THREE.Float32BufferAttribute(v, 3));
+
+ edge = new THREE.Line(geom, mtl);
+ mesh.add(edge);
+
+ edge = new THREE.Line(geom, mtl);
+ edge.position.z = h;
+ mesh.add(edge);
+
+ // vertical lines
+ for (j = 0, m = bnd.length - 1; j < m; j++) {
+ v = [bnd[j][0], bnd[j][1], 0,
+ bnd[j][0], bnd[j][1], h];
+
+ geom = new THREE.BufferGeometry();
+ geom.addAttribute("position", new THREE.Float32BufferAttribute(v, 3));
+
+ edge = new THREE.Line(geom, mtl);
+ mesh.add(edge);
+ }
+ }
+ }
+ return mesh;
+ };
+
+ createObject = function (f) {
+ if (f.geom.polygons.length == 1) return createSubObject(f, f.geom.polygons[0], f.geom.centroids[0][2]);
+
+ var group = new THREE.Group();
+ for (var i = 0, l = f.geom.polygons.length; i < l; i++) {
+ group.add(createSubObject(f, f.geom.polygons[i], f.geom.centroids[i][2]));
+ }
+ return group;
+ };
+ }
+ else if (this.properties.objType == "Overlay") {
+ createObject = function (f) {
+
+ var vertices = f.geom.triangles.v,
+ base_width = sceneData.width,
+ base_height = sceneData.height,
+ uvs = [];
+
+ for (var i = 0, l = vertices.length; i < l; i += 3) {
+ uvs.push(vertices[i] / base_width + 0.5, vertices[i + 1] / base_height + 0.5);
+ }
+
+ var geom = new THREE.BufferGeometry();
+ geom.setIndex(f.geom.triangles.f);
+ geom.addAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
+ geom.addAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
+ geom.computeVertexNormals();
+
+ var mesh = new THREE.Mesh(geom, materials.mtl(f.mtl.face));
+
+ // borders
+ if (f.geom.brdr !== undefined) {
+ var bnds, i, l, j, m;
+ for (i = 0, l = f.geom.brdr.length; i < l; i++) {
+ bnds = f.geom.brdr[i];
+ for (j = 0, m = bnds.length; j < m; j++) {
+ geom = new THREE.BufferGeometry(),
+ geom.addAttribute("position", new THREE.Float32BufferAttribute(bnds[j], 3));
+
+ mesh.add(new THREE.Line(geom, materials.mtl(f.mtl.brdr)));
+ }
+ }
+ }
+ return mesh;
+ };
+ }
+
+ // each feature in this layer
+ var f, obj;
+ for (var i = 0, l = features.length; i < l; i++) {
+ f = features[i];
+ obj = createObject(f);
+ obj.userData.properties = f.prop;
+
+ f.objIndices = [this.addObject(obj)];
+ }
+
+ this._lastObjType = this.properties.objType;
+ this._createObject = createObject;
+};
+
+Q3D.PolygonLayer.prototype.buildLabels = function (features) {
+ Q3D.VectorLayer.prototype.buildLabels.call(this, features, function (f) { return f.geom.centroids; });
+};
+
+Q3D.PolygonLayer.prototype.setBorderVisible = function (visible) {
+ if (this.properties.objType != "Overlay") return;
+
+ this.objectGroup.children.forEach(function (parent) {
+ for (var i = 0, l = parent.children.length; i < l; i++) {
+ var obj = parent.children[i];
+ if (obj instanceof THREE.Line) obj.visible = visible;
+ }
+ });
+ this.borderVisible = visible;
+};
+
+Q3D.PolygonLayer.prototype.setSideVisible = function (visible) {
+ if (this.properties.objType != "Overlay") return;
+
+ this.objectGroup.children.forEach(function (parent) {
+ for (var i = 0, l = parent.children.length; i < l; i++) {
+ var obj = parent.children[i];
+ if (obj instanceof THREE.Mesh) obj.visible = visible;
+ }
+ });
+ this.sideVisible = visible;
+};
+
+/*
+Q3D.Model
+*/
+Q3D.Model = function () {
+ this.loaded = false;
+};
+
+Q3D.Model.prototype = {
+
+ constructor: Q3D.Model,
+
+ // callback is called when model has been completely loaded
+ load: function (url, callback) {
+ var _this = this;
+ Q3D.application.loadModelFile(url, function (model) {
+ _this.model = model;
+ _this._loadCompleted(callback);
+ });
+ },
+
+ loadJSONObject: function (jsonObject, callback) {
+ this.load(jsonObject.url, callback);
+ },
+
+ _loadCompleted: function (anotherCallback) {
+ this.loaded = true;
+
+ if (this._callbacks !== undefined) {
+ for (var i = 0; i < this._callbacks.length; i++) {
+ this._callbacks[i](this.model);
+ }
+ this._callbacks = [];
+ }
+
+ if (anotherCallback) anotherCallback(this.model);
+ },
+
+ callbackOnLoad: function (callback) {
+ if (this.loaded) return callback(this.model);
+
+ if (this._callbacks === undefined) this._callbacks = [];
+ this._callbacks.push(callback);
+ }
+
+};
+
+
+/*
+Q3D.Models
+*/
+Q3D.Models = function () {
+ this.models = [];
+ this.cache = {};
+};
+
+Q3D.Models.prototype = Object.create(THREE.EventDispatcher.prototype);
+Q3D.Models.prototype.constructor = Q3D.Models;
+
+Q3D.Models.prototype.loadJSONObject = function (jsonObject) {
+ var _this = this;
+ var callback = function (model) {
+ _this.dispatchEvent({type: "modelLoaded", model: model});
+ };
+
+ var model, url;
+ for (var i = 0, l = jsonObject.length; i < l; i++) {
+ url = jsonObject[i].url;
+
+ if (this.cache[url] !== undefined) {
+ model = this.cache[url];
+ }
+ else {
+ model = new Q3D.Model();
+ model.load(url, callback);
+ this.cache[url] = model;
+ }
+
+ this.models.push(model);
+ }
+};
+
+Q3D.Models.prototype.get = function (index) {
+ return this.models[index];
+};
+
+Q3D.Models.prototype.clear = function () {
+ this.models = [];
+};
+
+
+// Q3D.Utils - Utilities
+Q3D.Utils = {};
+
+// Put a stick to given position (for debug)
+Q3D.Utils.putStick = function (x, y, zFunc, h) {
+ if (Q3D.Utils._stick_mat === undefined) Q3D.Utils._stick_mat = new THREE.LineBasicMaterial({color: 0xff0000});
+ if (h === undefined) h = 0.2;
+ if (zFunc === undefined) {
+ zFunc = function (x, y) { return Q3D.application.scene.mapLayers[0].getZ(x, y); };
+ }
+ var z = zFunc(x, y);
+ var geom = new THREE.Geometry();
+ geom.vertices.push(new THREE.Vector3(x, y, z + h), new THREE.Vector3(x, y, z));
+ var stick = new THREE.Line(geom, Q3D.Utils._stick_mat);
+ Q3D.application.scene.add(stick);
+};
+
+// convert latitude and longitude in degrees to the following format
+// Ndd°mm′ss.ss″, Eddd°mm′ss.ss″
+Q3D.Utils.convertToDMS = function (lat, lon) {
+ function toDMS(degrees) {
+ var deg = Math.floor(degrees),
+ m = (degrees - deg) * 60,
+ min = Math.floor(m),
+ sec = (m - min) * 60;
+ return deg + "°" + ("0" + min).slice(-2) + "′" + ((sec < 10) ? "0" : "") + sec.toFixed(2) + "″";
+ }
+
+ return ((lat < 0) ? "S" : "N") + toDMS(Math.abs(lat)) + ", " +
+ ((lon < 0) ? "W" : "E") + toDMS(Math.abs(lon));
+};
+
+Q3D.Utils.createWallGeometry = function (vertices, bzFunc, buffer_geom) {
+ var geom = new THREE.Geometry(),
+ pt = vertices[0];
+ geom.vertices.push(
+ new THREE.Vector3(pt.x, pt.y, pt.z),
+ new THREE.Vector3(pt.x, pt.y, bzFunc(pt.x, pt.y)));
+
+ for (var i = 1, i2 = 1, l = vertices.length; i < l; i++, i2+=2) {
+ pt = vertices[i];
+ geom.vertices.push(
+ new THREE.Vector3(pt.x, pt.y, pt.z),
+ new THREE.Vector3(pt.x, pt.y, bzFunc(pt.x, pt.y)));
+
+ geom.faces.push(
+ new THREE.Face3(i2 - 1, i2, i2 + 1),
+ new THREE.Face3(i2 + 1, i2, i2 + 2));
+ }
+ geom.computeFaceNormals();
+
+ if (buffer_geom || Q3D.Config.exportMode) {
+ return new THREE.BufferGeometry().fromGeometry(geom);
+ }
+ return geom;
+};
+
+Q3D.Utils.arrayToVec2Array = function (points) {
+ var pt, pts = [];
+ for (var i = 0, l = points.length; i < l; i++) {
+ pt = points[i];
+ pts.push(new THREE.Vector2(pt[0], pt[1]));
+ }
+ return pts;
+};
+
+Q3D.Utils.arrayToVec3Array = function (points, zFunc) {
+ var pt, pts = [];
+ if (zFunc === undefined) {
+ for (var i = 0, l = points.length; i < l; i++) {
+ pt = points[i];
+ pts.push(new THREE.Vector3(pt[0], pt[1], pt[2]));
+ }
+ }
+ else {
+ for (var i = 0, l = points.length; i < l; i++) {
+ pt = points[i];
+ pts.push(new THREE.Vector3(pt[0], pt[1], zFunc(pt[0], pt[1])));
+ }
+ }
+ return pts;
+};
+
+Q3D.Utils.arrayToFace3Array = function (faces) {
+ var f, fs = [];
+ for (var i = 0, l = faces.length; i < l; i++) {
+ f = faces[i];
+ fs.push(new THREE.Face3(f[0], f[1], f[2]));
+ }
+ return fs;
+};
+
+Q3D.Utils.setGeometryUVs = function (geom, base_width, base_height) {
+ var face, v, uvs = [];
+ for (var i = 0, l = geom.vertices.length; i < l; i++) {
+ v = geom.vertices[i];
+ uvs.push(new THREE.Vector2(v.x / base_width + 0.5, v.y / base_height + 0.5));
+ }
+
+ geom.faceVertexUvs[0] = [];
+ for (var i = 0, l = geom.faces.length; i < l; i++) {
+ face = geom.faces[i];
+ geom.faceVertexUvs[0].push([uvs[face.a], uvs[face.b], uvs[face.c]]);
+ }
+};
diff --git a/docs/examples/24/qgis2threejs.png b/docs/examples/24/qgis2threejs.png
new file mode 100644
index 00000000..1d003067
Binary files /dev/null and b/docs/examples/24/qgis2threejs.png differ
diff --git a/docs/examples/24/threejs/license b/docs/examples/24/threejs/license
new file mode 100644
index 00000000..d3400897
--- /dev/null
+++ b/docs/examples/24/threejs/license
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright © 2010-2019 three.js authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/docs/examples/24/threejs/orbitcontrols.js b/docs/examples/24/threejs/orbitcontrols.js
new file mode 100644
index 00000000..6df44d4e
--- /dev/null
+++ b/docs/examples/24/threejs/orbitcontrols.js
@@ -0,0 +1,1072 @@
+/**
+ * @author qiao / https://github.com/qiao
+ * @author mrdoob / http://mrdoob.com
+ * @author alteredq / http://alteredqualia.com/
+ * @author WestLangley / http://github.com/WestLangley
+ * @author erich666 / http://erichaines.com
+ */
+
+// This set of controls performs orbiting, dollying (zooming), and panning.
+// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
+//
+// Orbit - left mouse / touch: one-finger move
+// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
+// Pan - right mouse, or left mouse + ctrl/metaKey, or arrow keys / touch: two-finger move
+
+THREE.OrbitControls = function ( object, domElement ) {
+
+ this.object = object;
+
+ this.domElement = ( domElement !== undefined ) ? domElement : document;
+
+ // Set to false to disable this control
+ this.enabled = true;
+
+ // "target" sets the location of focus, where the object orbits around
+ this.target = new THREE.Vector3();
+
+ // How far you can dolly in and out ( PerspectiveCamera only )
+ this.minDistance = 0;
+ this.maxDistance = Infinity;
+
+ // How far you can zoom in and out ( OrthographicCamera only )
+ this.minZoom = 0;
+ this.maxZoom = Infinity;
+
+ // How far you can orbit vertically, upper and lower limits.
+ // Range is 0 to Math.PI radians.
+ this.minPolarAngle = 0; // radians
+ this.maxPolarAngle = Math.PI; // radians
+
+ // How far you can orbit horizontally, upper and lower limits.
+ // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
+ this.minAzimuthAngle = - Infinity; // radians
+ this.maxAzimuthAngle = Infinity; // radians
+
+ // Set to true to enable damping (inertia)
+ // If damping is enabled, you must call controls.update() in your animation loop
+ this.enableDamping = false;
+ this.dampingFactor = 0.25;
+
+ // This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
+ // Set to false to disable zooming
+ this.enableZoom = true;
+ this.zoomSpeed = 1.0;
+
+ // Set to false to disable rotating
+ this.enableRotate = true;
+ this.rotateSpeed = 1.0;
+
+ // Set to false to disable panning
+ this.enablePan = true;
+ this.panSpeed = 1.0;
+ this.screenSpacePanning = false; // if true, pan in screen-space
+ this.keyPanSpeed = 7.0; // pixels moved per arrow key push
+
+ // Set to true to automatically rotate around the target
+ // If auto-rotate is enabled, you must call controls.update() in your animation loop
+ this.autoRotate = false;
+ this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
+
+ // Set to false to disable use of the keys
+ this.enableKeys = true;
+
+ // The four arrow keys
+ this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
+
+ // Mouse buttons
+ this.mouseButtons = { LEFT: THREE.MOUSE.LEFT, MIDDLE: THREE.MOUSE.MIDDLE, RIGHT: THREE.MOUSE.RIGHT };
+
+ // for reset
+ this.target0 = this.target.clone();
+ this.position0 = this.object.position.clone();
+ this.zoom0 = this.object.zoom;
+
+ //
+ // public methods
+ //
+
+ this.getPolarAngle = function () {
+
+ return spherical.phi;
+
+ };
+
+ this.getAzimuthalAngle = function () {
+
+ return spherical.theta;
+
+ };
+
+ this.saveState = function () {
+
+ scope.target0.copy( scope.target );
+ scope.position0.copy( scope.object.position );
+ scope.zoom0 = scope.object.zoom;
+
+ };
+
+ this.reset = function () {
+
+ scope.target.copy( scope.target0 );
+ scope.object.position.copy( scope.position0 );
+ scope.object.zoom = scope.zoom0;
+
+ scope.object.updateProjectionMatrix();
+ scope.dispatchEvent( changeEvent );
+
+ scope.update();
+
+ state = STATE.NONE;
+
+ };
+
+ // this method is exposed, but perhaps it would be better if we can make it private...
+ this.update = function () {
+
+ var offset = new THREE.Vector3();
+
+ // so camera.up is the orbit axis
+ var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
+ var quatInverse = quat.clone().inverse();
+
+ var lastPosition = new THREE.Vector3();
+ var lastQuaternion = new THREE.Quaternion();
+
+ return function update() {
+
+ var position = scope.object.position;
+
+ offset.copy( position ).sub( scope.target );
+
+ // rotate offset to "y-axis-is-up" space
+ offset.applyQuaternion( quat );
+
+ // angle from z-axis around y-axis
+ spherical.setFromVector3( offset );
+
+ if ( scope.autoRotate && state === STATE.NONE ) {
+
+ rotateLeft( getAutoRotationAngle() );
+
+ }
+
+ spherical.theta += sphericalDelta.theta;
+ spherical.phi += sphericalDelta.phi;
+
+ // restrict theta to be between desired limits
+ spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) );
+
+ // restrict phi to be between desired limits
+ spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
+
+ spherical.makeSafe();
+
+
+ spherical.radius *= scale;
+
+ // restrict radius to be between desired limits
+ spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) );
+
+ // move target to panned location
+ scope.target.add( panOffset );
+
+ offset.setFromSpherical( spherical );
+
+ // rotate offset back to "camera-up-vector-is-up" space
+ offset.applyQuaternion( quatInverse );
+
+ position.copy( scope.target ).add( offset );
+
+ scope.object.lookAt( scope.target );
+
+ if ( scope.enableDamping === true ) {
+
+ sphericalDelta.theta *= ( 1 - scope.dampingFactor );
+ sphericalDelta.phi *= ( 1 - scope.dampingFactor );
+
+ panOffset.multiplyScalar( 1 - scope.dampingFactor );
+
+ } else {
+
+ sphericalDelta.set( 0, 0, 0 );
+
+ panOffset.set( 0, 0, 0 );
+
+ }
+
+ scale = 1;
+
+ // update condition is:
+ // min(camera displacement, camera rotation in radians)^2 > EPS
+ // using small-angle approximation cos(x/2) = 1 - x^2 / 8
+
+ if ( zoomChanged ||
+ lastPosition.distanceToSquared( scope.object.position ) > EPS ||
+ 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
+
+ scope.dispatchEvent( changeEvent );
+
+ lastPosition.copy( scope.object.position );
+ lastQuaternion.copy( scope.object.quaternion );
+ zoomChanged = false;
+
+ return true;
+
+ }
+
+ return false;
+
+ };
+
+ }();
+
+ this.dispose = function () {
+
+ scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false );
+ scope.domElement.removeEventListener( 'mousedown', onMouseDown, false );
+ scope.domElement.removeEventListener( 'wheel', onMouseWheel, false );
+
+ scope.domElement.removeEventListener( 'touchstart', onTouchStart, false );
+ scope.domElement.removeEventListener( 'touchend', onTouchEnd, false );
+ scope.domElement.removeEventListener( 'touchmove', onTouchMove, false );
+
+ document.removeEventListener( 'mousemove', onMouseMove, false );
+ document.removeEventListener( 'mouseup', onMouseUp, false );
+
+ window.removeEventListener( 'keydown', onKeyDown, false );
+
+ //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
+
+ };
+
+ //
+ // internals
+ //
+
+ var scope = this;
+
+ var changeEvent = { type: 'change' };
+ var startEvent = { type: 'start' };
+ var endEvent = { type: 'end' };
+
+ var STATE = { NONE: - 1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY_PAN: 4 };
+
+ var state = STATE.NONE;
+
+ var EPS = 0.000001;
+
+ // current position in spherical coordinates
+ var spherical = new THREE.Spherical();
+ var sphericalDelta = new THREE.Spherical();
+
+ var scale = 1;
+ var panOffset = new THREE.Vector3();
+ var zoomChanged = false;
+
+ var rotateStart = new THREE.Vector2();
+ var rotateEnd = new THREE.Vector2();
+ var rotateDelta = new THREE.Vector2();
+
+ var panStart = new THREE.Vector2();
+ var panEnd = new THREE.Vector2();
+ var panDelta = new THREE.Vector2();
+
+ var dollyStart = new THREE.Vector2();
+ var dollyEnd = new THREE.Vector2();
+ var dollyDelta = new THREE.Vector2();
+
+ function getAutoRotationAngle() {
+
+ return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
+
+ }
+
+ function getZoomScale() {
+
+ return Math.pow( 0.95, scope.zoomSpeed );
+
+ }
+
+ function rotateLeft( angle ) {
+
+ sphericalDelta.theta -= angle;
+
+ }
+
+ function rotateUp( angle ) {
+
+ sphericalDelta.phi -= angle;
+
+ }
+
+ var panLeft = function () {
+
+ var v = new THREE.Vector3();
+
+ return function panLeft( distance, objectMatrix ) {
+
+ v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
+ v.multiplyScalar( - distance );
+
+ panOffset.add( v );
+
+ };
+
+ }();
+
+ var panUp = function () {
+
+ var v = new THREE.Vector3();
+
+ return function panUp( distance, objectMatrix ) {
+
+ if ( scope.screenSpacePanning === true ) {
+
+ v.setFromMatrixColumn( objectMatrix, 1 );
+
+ } else {
+
+ v.setFromMatrixColumn( objectMatrix, 0 );
+ v.crossVectors( scope.object.up, v );
+
+ }
+
+ v.multiplyScalar( distance );
+
+ panOffset.add( v );
+
+ };
+
+ }();
+
+ // deltaX and deltaY are in pixels; right and down are positive
+ var pan = function () {
+
+ var offset = new THREE.Vector3();
+
+ return function pan( deltaX, deltaY ) {
+
+ var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+
+ if ( scope.object.isPerspectiveCamera ) {
+
+ // perspective
+ var position = scope.object.position;
+ offset.copy( position ).sub( scope.target );
+ var targetDistance = offset.length();
+
+ // half of the fov is center to top of screen
+ targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
+
+ // we use only clientHeight here so aspect ratio does not distort speed
+ panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
+ panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
+
+ } else if ( scope.object.isOrthographicCamera ) {
+
+ // orthographic
+ panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
+ panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
+
+ } else {
+
+ // camera neither orthographic nor perspective
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
+ scope.enablePan = false;
+
+ }
+
+ };
+
+ }();
+
+ function dollyIn( dollyScale ) {
+
+ if ( scope.object.isPerspectiveCamera ) {
+
+ scale /= dollyScale;
+
+ } else if ( scope.object.isOrthographicCamera ) {
+
+ scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
+ scope.object.updateProjectionMatrix();
+ zoomChanged = true;
+
+ } else {
+
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+ scope.enableZoom = false;
+
+ }
+
+ }
+
+ function dollyOut( dollyScale ) {
+
+ if ( scope.object.isPerspectiveCamera ) {
+
+ scale *= dollyScale;
+
+ } else if ( scope.object.isOrthographicCamera ) {
+
+ scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
+ scope.object.updateProjectionMatrix();
+ zoomChanged = true;
+
+ } else {
+
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+ scope.enableZoom = false;
+
+ }
+
+ }
+
+ //
+ // event callbacks - update the object state
+ //
+
+ function handleMouseDownRotate( event ) {
+
+ //console.log( 'handleMouseDownRotate' );
+
+ rotateStart.set( event.clientX, event.clientY );
+
+ }
+
+ function handleMouseDownDolly( event ) {
+
+ //console.log( 'handleMouseDownDolly' );
+
+ dollyStart.set( event.clientX, event.clientY );
+
+ }
+
+ function handleMouseDownPan( event ) {
+
+ //console.log( 'handleMouseDownPan' );
+
+ panStart.set( event.clientX, event.clientY );
+
+ }
+
+ function handleMouseMoveRotate( event ) {
+
+ //console.log( 'handleMouseMoveRotate' );
+
+ rotateEnd.set( event.clientX, event.clientY );
+
+ rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
+
+ var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+
+ rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
+
+ rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
+
+ rotateStart.copy( rotateEnd );
+
+ scope.update();
+
+ }
+
+ function handleMouseMoveDolly( event ) {
+
+ //console.log( 'handleMouseMoveDolly' );
+
+ dollyEnd.set( event.clientX, event.clientY );
+
+ dollyDelta.subVectors( dollyEnd, dollyStart );
+
+ if ( dollyDelta.y > 0 ) {
+
+ dollyIn( getZoomScale() );
+
+ } else if ( dollyDelta.y < 0 ) {
+
+ dollyOut( getZoomScale() );
+
+ }
+
+ dollyStart.copy( dollyEnd );
+
+ scope.update();
+
+ }
+
+ function handleMouseMovePan( event ) {
+
+ //console.log( 'handleMouseMovePan' );
+
+ panEnd.set( event.clientX, event.clientY );
+
+ panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
+
+ pan( panDelta.x, panDelta.y );
+
+ panStart.copy( panEnd );
+
+ scope.update();
+
+ }
+
+ function handleMouseUp( event ) {
+
+ // console.log( 'handleMouseUp' );
+
+ }
+
+ function handleMouseWheel( event ) {
+
+ // console.log( 'handleMouseWheel' );
+
+ var delta = 0;
+
+ if ( event.deltaY !== undefined ) {
+
+ delta = event.deltaY;
+
+ } else if ( event.wheelDelta !== undefined ) { // WebKit @minorua
+
+ delta = - event.wheelDelta;
+
+ }
+
+ if ( delta < 0 ) {
+
+ dollyOut( getZoomScale() );
+
+ } else if ( delta > 0 ) {
+
+ dollyIn( getZoomScale() );
+
+ }
+
+ scope.update();
+
+ }
+
+ function handleKeyDown( event ) {
+
+ //console.log( 'handleKeyDown' );
+
+ switch ( event.keyCode ) {
+
+ case scope.keys.UP:
+ pan( 0, scope.keyPanSpeed );
+ scope.update();
+ break;
+
+ case scope.keys.BOTTOM:
+ pan( 0, - scope.keyPanSpeed );
+ scope.update();
+ break;
+
+ case scope.keys.LEFT:
+ pan( scope.keyPanSpeed, 0 );
+ scope.update();
+ break;
+
+ case scope.keys.RIGHT:
+ pan( - scope.keyPanSpeed, 0 );
+ scope.update();
+ break;
+
+ }
+
+ }
+
+ function handleTouchStartRotate( event ) {
+
+ //console.log( 'handleTouchStartRotate' );
+
+ rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+ }
+
+ function handleTouchStartDollyPan( event ) {
+
+ //console.log( 'handleTouchStartDollyPan' );
+
+ if ( scope.enableZoom ) {
+
+ var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+
+ var distance = Math.sqrt( dx * dx + dy * dy );
+
+ dollyStart.set( 0, distance );
+
+ }
+
+ if ( scope.enablePan ) {
+
+ var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
+ var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
+
+ panStart.set( x, y );
+
+ }
+
+ }
+
+ function handleTouchMoveRotate( event ) {
+
+ //console.log( 'handleTouchMoveRotate' );
+
+ rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+ rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
+
+ var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+
+ rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
+
+ rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
+
+ rotateStart.copy( rotateEnd );
+
+ scope.update();
+
+ }
+
+ function handleTouchMoveDollyPan( event ) {
+
+ //console.log( 'handleTouchMoveDollyPan' );
+
+ if ( scope.enableZoom ) {
+
+ var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+
+ var distance = Math.sqrt( dx * dx + dy * dy );
+
+ dollyEnd.set( 0, distance );
+
+ dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );
+
+ dollyIn( dollyDelta.y );
+
+ dollyStart.copy( dollyEnd );
+
+ }
+
+ if ( scope.enablePan ) {
+
+ var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
+ var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
+
+ panEnd.set( x, y );
+
+ panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
+
+ pan( panDelta.x, panDelta.y );
+
+ panStart.copy( panEnd );
+
+ }
+
+ scope.update();
+
+ }
+
+ function handleTouchEnd( event ) {
+
+ //console.log( 'handleTouchEnd' );
+
+ }
+
+ //
+ // event handlers - FSM: listen for events and reset state
+ //
+
+ function onMouseDown( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ event.preventDefault();
+
+ switch ( event.button ) {
+
+ case scope.mouseButtons.LEFT:
+
+ if ( event.ctrlKey || event.metaKey ) {
+
+ if ( scope.enablePan === false ) return;
+
+ handleMouseDownPan( event );
+
+ state = STATE.PAN;
+
+ } else {
+
+ if ( scope.enableRotate === false ) return;
+
+ handleMouseDownRotate( event );
+
+ state = STATE.ROTATE;
+
+ }
+
+ break;
+
+ case scope.mouseButtons.MIDDLE:
+
+ if ( scope.enableZoom === false ) return;
+
+ handleMouseDownDolly( event );
+
+ state = STATE.DOLLY;
+
+ break;
+
+ case scope.mouseButtons.RIGHT:
+
+ if ( scope.enablePan === false ) return;
+
+ handleMouseDownPan( event );
+
+ state = STATE.PAN;
+
+ break;
+
+ }
+
+ if ( state !== STATE.NONE ) {
+
+ document.addEventListener( 'mousemove', onMouseMove, false );
+ document.addEventListener( 'mouseup', onMouseUp, false );
+
+ scope.dispatchEvent( startEvent );
+
+ }
+
+ }
+
+ function onMouseMove( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ event.preventDefault();
+
+ switch ( state ) {
+
+ case STATE.ROTATE:
+
+ if ( scope.enableRotate === false ) return;
+
+ handleMouseMoveRotate( event );
+
+ break;
+
+ case STATE.DOLLY:
+
+ if ( scope.enableZoom === false ) return;
+
+ handleMouseMoveDolly( event );
+
+ break;
+
+ case STATE.PAN:
+
+ if ( scope.enablePan === false ) return;
+
+ handleMouseMovePan( event );
+
+ break;
+
+ }
+
+ }
+
+ function onMouseUp( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ handleMouseUp( event );
+
+ document.removeEventListener( 'mousemove', onMouseMove, false );
+ document.removeEventListener( 'mouseup', onMouseUp, false );
+
+ scope.dispatchEvent( endEvent );
+
+ state = STATE.NONE;
+
+ }
+
+ function onMouseWheel( event ) {
+
+ if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ scope.dispatchEvent( startEvent );
+
+ handleMouseWheel( event );
+
+ scope.dispatchEvent( endEvent );
+
+ }
+
+ function onKeyDown( event ) {
+
+ if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return;
+
+ handleKeyDown( event );
+
+ }
+
+ function onTouchStart( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ // event.preventDefault(); // comment out @minorua
+
+ switch ( event.touches.length ) {
+
+ case 1: // one-fingered touch: rotate
+
+ if ( scope.enableRotate === false ) return;
+
+ handleTouchStartRotate( event );
+
+ state = STATE.TOUCH_ROTATE;
+
+ break;
+
+ case 2: // two-fingered touch: dolly-pan
+
+ if ( scope.enableZoom === false && scope.enablePan === false ) return;
+
+ handleTouchStartDollyPan( event );
+
+ state = STATE.TOUCH_DOLLY_PAN;
+
+ break;
+
+ default:
+
+ state = STATE.NONE;
+
+ }
+
+ if ( state !== STATE.NONE ) {
+
+ scope.dispatchEvent( startEvent );
+
+ }
+
+ }
+
+ function onTouchMove( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ switch ( event.touches.length ) {
+
+ case 1: // one-fingered touch: rotate
+
+ if ( scope.enableRotate === false ) return;
+ if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?
+
+ handleTouchMoveRotate( event );
+
+ break;
+
+ case 2: // two-fingered touch: dolly-pan
+
+ if ( scope.enableZoom === false && scope.enablePan === false ) return;
+ if ( state !== STATE.TOUCH_DOLLY_PAN ) return; // is this needed?
+
+ handleTouchMoveDollyPan( event );
+
+ break;
+
+ default:
+
+ state = STATE.NONE;
+
+ }
+
+ }
+
+ function onTouchEnd( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ handleTouchEnd( event );
+
+ scope.dispatchEvent( endEvent );
+
+ state = STATE.NONE;
+
+ }
+
+ function onContextMenu( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ event.preventDefault();
+
+ }
+
+ // expose functions @minorua
+ this.panLeft = panLeft;
+ this.rotateLeft = rotateLeft;
+ this.rotateUp = rotateUp;
+ this.dollyIn = dollyIn;
+ this.dollyOut = dollyOut;
+ this.getZoomScale = getZoomScale;
+
+ //
+
+ scope.domElement.addEventListener( 'contextmenu', onContextMenu, false );
+
+ scope.domElement.addEventListener( 'mousedown', onMouseDown, false );
+ scope.domElement.addEventListener( 'wheel', onMouseWheel, false );
+ scope.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); // WebKit @minorua
+
+ scope.domElement.addEventListener( 'touchstart', onTouchStart, false );
+ scope.domElement.addEventListener( 'touchend', onTouchEnd, false );
+ scope.domElement.addEventListener( 'touchmove', onTouchMove, false );
+
+ window.addEventListener( 'keydown', onKeyDown, false );
+
+ // force an update at start
+
+ this.update();
+
+};
+
+THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
+THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;
+
+Object.defineProperties( THREE.OrbitControls.prototype, {
+
+ center: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .center has been renamed to .target' );
+ return this.target;
+
+ }
+
+ },
+
+ // backward compatibility
+
+ noZoom: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
+ return ! this.enableZoom;
+
+ },
+
+ set: function ( value ) {
+
+ console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
+ this.enableZoom = ! value;
+
+ }
+
+ },
+
+ noRotate: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
+ return ! this.enableRotate;
+
+ },
+
+ set: function ( value ) {
+
+ console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
+ this.enableRotate = ! value;
+
+ }
+
+ },
+
+ noPan: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
+ return ! this.enablePan;
+
+ },
+
+ set: function ( value ) {
+
+ console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
+ this.enablePan = ! value;
+
+ }
+
+ },
+
+ noKeys: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
+ return ! this.enableKeys;
+
+ },
+
+ set: function ( value ) {
+
+ console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
+ this.enableKeys = ! value;
+
+ }
+
+ },
+
+ staticMoving: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
+ return ! this.enableDamping;
+
+ },
+
+ set: function ( value ) {
+
+ console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
+ this.enableDamping = ! value;
+
+ }
+
+ },
+
+ dynamicDampingFactor: {
+
+ get: function () {
+
+ console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
+ return this.dampingFactor;
+
+ },
+
+ set: function ( value ) {
+
+ console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
+ this.dampingFactor = value;
+
+ }
+
+ }
+
+} );
diff --git a/docs/examples/24/threejs/three.min.js b/docs/examples/24/threejs/three.min.js
new file mode 100644
index 00000000..af2a3848
--- /dev/null
+++ b/docs/examples/24/threejs/three.min.js
@@ -0,0 +1,996 @@
+// threejs.org/license
+(function(k,xa){"object"===typeof exports&&"undefined"!==typeof module?xa(exports):"function"===typeof define&&define.amd?define(["exports"],xa):(k=k||self,xa(k.THREE={}))})(this,function(k){function xa(){}function v(a,b){this.x=a||0;this.y=b||0}function na(a,b,c,d){this._x=a||0;this._y=b||0;this._z=c||0;this._w=void 0!==d?d:1}function n(a,b,c){this.x=a||0;this.y=b||0;this.z=c||0}function ta(){this.elements=[1,0,0,0,1,0,0,0,1];0n!==t.next.y>n&&t.next.y!==t.y&&k<(t.next.x-t.x)*(n-t.y)/(t.next.y-t.y)+t.x&&(p=!p),t=t.next;while(t!==l);t=p}l=t}if(l){a=Oh(g,h);g=$d(g,g.next);a=$d(a,a.next);ae(g,b,c,d,e,f);ae(a,b,c,d,e,f);break a}h=
+h.next}g=g.next}while(g!==a)}break}}}}function ak(a,b,c,d){var e=a.prev,f=a.next;if(0<=ua(e,a,f))return!1;var g=e.x>a.x?e.x>f.x?e.x:f.x:a.x>f.x?a.x:f.x,h=e.y>a.y?e.y>f.y?e.y:f.y:a.y>f.y?a.y:f.y,l=ig(e.x