window.mat4 = glMatrix.mat4 window.vec2 = glMatrix.vec2 window.vec3 = glMatrix.vec3 // read in 512KB slices var chunk_size = 512 * 1024; var strict = false; var options = { trim: false, normalize: false, xmlns: false, position: false, strictEntities: true }; const State = { SEARCH_FOR_SURFACE_OR_GEOMETRY: 0, READ_COORDINATES: 3, } var currentState = State.SEARCH_FOR_SURFACE_OR_GEOMETRY; var color = [1.0, 1.0, 1.0]; var axis = vec3.fromValues(19, 0.8, 1.5); vec3.normalize(axis, axis); const SCROLL_PERCENTAGE = 0.001; var parserFile; var fileSize; var offset = 0; var currentPolygon = []; var coordinateString = ""; var triangulatedPolygons = []; var viewing; var bbox; var clearColor = new Float32Array([0.9411765, 1.0, 1.0, 1.0]); var clearDepth = new Float32Array([1.0]); var camera; var width = 0; var height = 0; var skipFirstResizeEvent = false; var mouseX; var mouseY; var fromProjection; var toProjection; var canvas = document.getElementById("viewport"); var progress = document.getElementById("progress"); var gl = canvas.getContext('webgl2', { antialias: true }); var isWebGL2 = !!gl; if (!isWebGL2) { alert('WebGL 2 is not available. See https://www.khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">How to get a WebGL 2 implementation'); } canvas.oncontextmenu = function () { return false; }; canvas.onwheel = function (event) { if (camera === undefined) { return; } event.preventDefault(); camera.scroll(event.deltaY); redraw(gl); }; canvas.onmousedown = function (event) { if (camera === undefined) { return; } event.preventDefault(); if (event.buttons === 1 || event.buttons === 2) { mouseX = event.layerX; mouseY = event.layerY; } }; canvas.onmousemove = function (event) { if (camera === undefined) { return; } event.preventDefault(); if (event.buttons === 1) { var deltaX = event.layerX - mouseX; var deltaY = event.layerY - mouseY; mouseX = event.layerX; mouseY = event.layerY; camera.rotate(deltaX, deltaY); redraw(gl); } else if (event.buttons === 2) { var deltaX = event.layerX - mouseX; var deltaY = event.layerY - mouseY; mouseX = event.layerX; mouseY = event.layerY; camera.move(deltaX, deltaY); redraw(gl); } }; const resizeObserver = new ResizeObserver(entries => { if (skipFirstResizeEvent) { skipFirstResizeEvent = false; return; } for (let entry of entries) { if (entry.contentBoxSize) { if (entry.contentBoxSize[0]) { height = entry.contentBoxSize[0].blockSize; width = entry.contentBoxSize[0].inlineSize; } else { height = entry.contentBoxSize.blockSize; width = entry.contentBoxSize.inlineSize; } } else { width = entry.contentRect.width; height = entry.contentRect.height; } } resizeObserver.unobserve(canvas); skipFirstResizeEvent = true; canvas.height = height; canvas.width = width; resizeObserver.observe(canvas); if (camera === undefined) { return; } gl.viewport(0, 0, width, height); camera.reshape(width, height); redraw(gl); }); resizeObserver.observe(canvas); parser = sax.parser(strict, options); parser.ontext = function (t) { if (currentState === State.READ_COORDINATES) { coordinateString += t; } }; parser.onattribute = function (attr) { if (attr.name === "SRSNAME") { fromProjection = proj4('EPSG:4326'); if (fromProjection.oProj.units === "degrees") { } } } parser.onopentag = function (node) { var tagName = node.name.substring(node.name.indexOf(':') + 1); switch (currentState) { case State.SEARCH_FOR_SURFACE_OR_GEOMETRY: var tempColor = getColorForTagName(tagName); if (tempColor !== undefined) { color = tempColor } else if (tagName.startsWith("LOD")) { var lodChar = tagName.charAt(3); if (lodChar === "1") { } else if (lodChar === "2") { } else if (lodChar === "3") { } else if (lodChar === "4") { } else if (lodChar === "0") { } } else if (tagName === "EXTERIOR" || tagName === "INTERIOR") { currentState = State.READ_COORDINATES; } break; } }; function getColorForTagName(tagName) { switch (tagName) { case "GROUNDSURFACE": return [0.9411765, 0.9019608, 0.54901963]; case "ROOFSURFACE": return [1.0, 0.0, 0.0]; case "DOOR": return [1.0, 0.784313, 0.0]; case "WINDOW": return [0.0, 0.5019608, 0.5019608]; case "WATERBODY": return [0.5294118, 0.80784315, 0.98039216]; case "BRIDGE": case "BRIDGEPART": return [1, 0.49803922, 0.3137255]; case "PLANTCOVER": case "SOLITARYVEGETATIONOBJECT": return [0.5647059, 0.93333334, 0.5647059]; case "INTERSECTION": case "ROAD": case "RAILWAY": case "SECTION": case "SQUARE": case "TRACK": case "TRANSPORTATIONCOMPLEX": case "WATERWAY": return [0.4, 0.4, 0.4]; } return undefined; }; parser.onclosetag = function (node) { var tagName = node.substring(node.indexOf(':') + 1); switch (currentState) { case State.READ_COORDINATES: if (tagName === "EXTERIOR" || tagName === "INTERIOR") { var split = coordinateString.trim().split(" "); var coords = []; for (var i = 0; i < split.length; i = i + 3) { var x = parseFloat(split[i]); var y = parseFloat(split[i + 1]); var z = parseFloat(split[i + 2]); bbox.expandX(x); bbox.expandY(y); bbox.expandZ(z); coords.push([x, y, z]); } currentPolygon.push(coords); coordinateString = ""; } else if (tagName === "POLYGON") { // no more interior rings var triangles = triangulate(currentPolygon); var cos = vec3.dot(triangles.normal, axis); var acos = Math.acos(cos); acos = acos / Math.PI; acos = acos * 0.6 + 0.3; var derivedColor = [color[0] * acos, color[1] * acos, color[2] * acos]; var poly = { triangles: triangles.vertices, color: derivedColor, }; triangulatedPolygons.push(poly); currentPolygon = []; currentState = State.SEARCH_FOR_SURFACE_OR_GEOMETRY; } else if (tagName === "POS") { coordinateString += " "; } break; case State.SEARCH_FOR_SURFACE_OR_GEOMETRY: switch (tagName) { case "GROUNDSURFACE": case "ROOFSURFACE": case "DOOR": case "WINDOW": case "WATERBODY": case "BRIDGE": case "BRIDGEPART": case "PLANTCOVER": case "SOLITARYVEGETATIONOBJECT": case "INTERSECTION": case "ROAD": case "RAILWAY": case "SECTION": case "SQUARE": case "TRACK": case "TRANSPORTATIONCOMPLEX": case "WATERWAY": color = [1.0, 1.0, 1.0]; break; } break; } } parser.onend = function () { gl.enable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE); gl.viewport(0, 0, width, height); viewing = []; createViewInformation(gl, viewing); var program = createProgram(gl, getShaderSource('vs'), getShaderSource('fs')); gl.useProgram(program); camera = new Camera(program, gl); var cameraViewDistance = 10000; camera.reshape(width, height, cameraViewDistance); var d = bbox.getDiagonalEdgeLength() / Math.tan(degrees_to_radians(30) / 2); var translateZ = -d; camera.setDistance(translateZ); camera.rotate(Math.PI / 2 * 500, 300); document.getElementById("progressDiv").style.display = "none"; redraw(gl); }; function createViewInformation(gl, viewing) { const center = bbox.getCenter(); var vertexDataCount = 0; for (const poly of triangulatedPolygons) { vertexDataCount += poly.triangles.length; } var vertexData = new Float32Array(vertexDataCount); var colorData = new Float32Array(vertexDataCount); var elementData = new Float32Array(vertexDataCount / 3); var counter = 0; for (const poly of triangulatedPolygons) { for (var i = 0; i < poly.triangles.length; i++) { vertexData[counter] = poly.triangles[i] - center[i % 3]; colorData[counter] = poly.color[i % 3]; counter++; } } for (var i = 0; i < elementData.length; i++) { elementData[i] = i; } var viewInformation = {}; viewInformation.elements = elementData.length; viewInformation.draw = true; viewInformation.vao = gl.createVertexArray(); gl.bindVertexArray(viewInformation.vao); viewInformation.posVbo = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, viewInformation.posVbo); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW); viewInformation.colorVbo = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, viewInformation.colorVbo); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0); gl.bufferData(gl.ARRAY_BUFFER, colorData, gl.STATIC_DRAW); viewInformation.indexVbo = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, viewInformation.indexVbo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, elementData, gl.STATIC_DRAW); gl.bindVertexArray(null); viewing.push(viewInformation); } function redraw(gl) { gl.clearBufferfv(gl.COLOR, 0, clearColor); gl.clearBufferfv(gl.DEPTH, 0, clearDepth); gl.bindVertexArray(viewing[0].vao); gl.drawArrays(gl.TRIANGLES, 0, viewing[0].elements); } class BBox { constructor() { this.lowerCorner = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]; this.upperCorner = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY]; } expandX(x) { if (this.lowerCorner[0] > x) { this.lowerCorner[0] = x; } if (this.upperCorner[0] < x) { this.upperCorner[0] = x; } } expandY(y) { if (this.lowerCorner[1] > y) { this.lowerCorner[1] = y; } if (this.upperCorner[1] < y) { this.upperCorner[1] = y; } } expandZ(z) { if (this.lowerCorner[2] > z) { this.lowerCorner[2] = z; } if (this.upperCorner[2] < z) { this.upperCorner[2] = z; } } getDiagonalEdgeLength() { var xDif = this.upperCorner[0] - this.lowerCorner[0]; var yDif = this.upperCorner[1] - this.lowerCorner[1]; var zDif = this.upperCorner[2] - this.lowerCorner[2]; return Math.sqrt(xDif * xDif + yDif * yDif + zDif * zDif) * 0.2; } getCenter() { var x = (this.upperCorner[0] + this.lowerCorner[0]) / 2.0; var y = (this.upperCorner[1] + this.lowerCorner[1]) / 2.0; var z = (this.upperCorner[2] + this.lowerCorner[2]) / 2.0; return [x, y, z]; } } class Camera { constructor(shader, gl) { this.gl = gl; this.uniformLocation = gl.getUniformLocation(shader, "projViewModel"); this.up = vec3.create(); this.up[2] = 1; this.distance = 5; this.eyepos = vec3.create(); this.eyepos[0] = this.distance; this.centerPos = vec3.create(); this.projMatrix = mat4.create(); this.viewMatrix = mat4.create(); this.projViewMatrix = mat4.create(); this.rotateAroundZ = 0; this.origin = vec3.create(); } reshape(width, height, distance) { const fow = Math.PI / 2; const aspectRatio = width * 1.0 / height; mat4.perspective(this.projMatrix, fow, aspectRatio, 1, distance); this.updateMatrix(); } updateMatrix() { mat4.lookAt(this.viewMatrix, this.eyepos, this.centerPos, this.up); mat4.multiply(this.projViewMatrix, this.projMatrix, this.viewMatrix); this.gl.uniformMatrix4fv(this.uniformLocation, false, this.projViewMatrix); } rotate(dragDiffX, dragDiffY) { var rotationX = -dragDiffX * 1.0 / 500; var rotationZ = -dragDiffY * 1.0 / 500; var tempRotationValue = this.rotateAroundZ + rotationZ; if (tempRotationValue < -Math.PI / 2 + 0.0001 || tempRotationValue > Math.PI / 2 - 0.0001) { // to close to 90 degree, stop rotation rotationZ = 0; } this.rotateAroundZ += rotationZ; var res = vec3.create(); vec3.sub(res, this.eyepos, this.centerPos); vec3.rotateZ(res, res, this.origin, rotationX); var rotAxis = vec3.fromValues(res[0], res[1], res[2] + 5); vec3.cross(rotAxis, rotAxis, res); vec3.normalize(rotAxis, rotAxis); this.rotateAroundAxis(res, rotationZ, rotAxis[0], rotAxis[1], rotAxis[2]); vec3.add(this.eyepos, this.centerPos, res); this.updateMatrix(); } rotateAroundAxis(vector, angle, aX, aY, aZ) { var hangle = angle * 0.5; var sinAngle = Math.sin(hangle); var qx = aX * sinAngle, qy = aY * sinAngle, qz = aZ * sinAngle; var qw = Math.cos(hangle); var w2 = qw * qw, x2 = qx * qx, y2 = qy * qy, z2 = qz * qz, zw = qz * qw; var xy = qx * qy, xz = qx * qz, yw = qy * qw, yz = qy * qz, xw = qx * qw; var x = vector[0], y = vector[1], z = vector[2]; vector[0] = (w2 + x2 - z2 - y2) * x + (-zw + xy - zw + xy) * y + (yw + xz + xz + yw) * z; vector[1] = (xy + zw + zw + xy) * x + (y2 - z2 + w2 - x2) * y + (yz + yz - xw - xw) * z; vector[2] = (xz - yw + xz - yw) * x + (yz + yz + xw + xw) * y + (z2 - y2 - x2 + w2) * z; } setDistance(distance) { this.distance = distance; var res = vec3.create(); vec3.subtract(res, this.centerPos, this.eyepos); vec3.normalize(res, res); vec3.scale(res, res, this.distance); vec3.add(this.eyepos, res, this.centerPos); this.updateMatrix(); } scroll(scroll) { var addedDistance = this.distance * scroll * SCROLL_PERCENTAGE; this.distance += addedDistance; this.setDistance(this.distance); } move(dragDiffX, dragDiffY) { var res = vec3.create(); vec3.sub(res, this.centerPos, this.eyepos); var dir = vec2.fromValues(res[0], res[1]); vec2.normalize(dir, dir); var yDrag = vec2.create(); vec2.scale(yDrag, dir, dragDiffY * 0.5); // handle diffY vec3.add(this.centerPos, this.centerPos, vec3.fromValues(yDrag[0], yDrag[1], 0)); vec3.add(this.eyepos, this.eyepos, vec3.fromValues(yDrag[0], yDrag[1], 0)); // handle diffX var temp = dir[0]; dir[0] = dir[1]; dir[1] = -temp; vec2.scale(dir, dir, -dragDiffX * 0.5); vec3.add(this.centerPos, this.centerPos, vec3.fromValues(dir[0], dir[1], 0)); vec3.add(this.eyepos, this.eyepos, vec3.fromValues(dir[0], dir[1], 0)); this.updateMatrix(); } } function degrees_to_radians(degrees) { // Store the value of pi. var pi = Math.PI; // Multiply degrees by pi divided by 180 to convert to radians. return degrees * (pi / 180); } function getShaderSource(id) { var content = document.getElementById(id).textContent; return content.replace(/^\s+|\s+$/g, ''); } function createShader(gl, source, type) { var shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); return shader; } function createProgram(gl, vertexShaderSource, fragmentShaderSource) { var program = gl.createProgram(); var vshader = createShader(gl, vertexShaderSource, gl.VERTEX_SHADER); var fshader = createShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER); gl.attachShader(program, vshader); gl.deleteShader(vshader); gl.attachShader(program, fshader); gl.deleteShader(fshader); gl.linkProgram(program); return program; }; var input = document.getElementById('input'); input.addEventListener("change", function () { if (this.files && this.files[0]) { camera = undefined; parserFile = this.files[0]; fileSize = parserFile.size; offset = 0; bbox = new BBox(); setProgress(0); document.getElementById("progressDiv").style.removeProperty("display"); readSlice(); } }); function setProgress(progressValue) { if (progressValue > 1) { progressValue = 1; } progressValue = Math.floor(progressValue * 100); var fn = function() { progress.setAttribute("style", "width: " + Number(progressValue) + "%"); progress.ariaValueNow = progressValue; progress.innerHTML = progressValue + "%"; } setTimeout(fn); } function readSlice() { setProgress(offset * 1.0 / fileSize); if (offset >= fileSize) { // nothing to read anymore parser.close(); return; } var blob = parserFile.slice(offset, offset + chunk_size); offset += chunk_size; blob.text().then(t => { parser.write(t); readSlice(); }); } function triangulate(polygon) { var triangleVerts = []; tessy.gluTessBeginPolygon(triangleVerts); var exterior = polygon[0]; var normal = calculateNormal(polygon[0]); tessy.gluTessNormal(normal[0], normal[1], normal[2]); tessy.gluTessBeginContour(); var contour = polygon[i]; for (var j = 0; j < exterior.length; j++) { tessy.gluTessVertex(exterior[j], exterior[j]); } tessy.gluTessEndContour(); for (var i = 1; i < polygon.length; i++) { tessy.gluTessBeginContour(); var contour = polygon[i]; for (var j = 0; j < contour.length; j++) { tessy.gluTessVertex(contour[j], contour[j]); } tessy.gluTessEndContour(); } tessy.gluTessEndPolygon(); return { vertices: triangleVerts, normal: normal } } function calculateNormal(ring) { var coords = [0, 0, 0]; for (var i = 0; i < ring.length - 1; i++) { var current = ring[i + 0]; var next = ring[i + 1]; coords[0] += (current[2] + next[2]) * (current[1] - next[1]); coords[1] += (current[0] + next[0]) * (current[2] - next[2]); coords[2] += (current[1] + next[1]) * (current[0] - next[0]); } if (coords[0] == 0 && coords[1] == 0 && coords[2] == 0) { // no valid normal vector found if (ring.length < 3) { // no three points, return x-axis return vec3.create([1, 0, 0]); } var v1 = ring[0]; var v2 = ring[1]; var v3 = ring[2]; return calculateNormalWithCross(vec3.create(v1), vec3.create(v2), vec3.create(v3)); } var v = vec3.fromValues(coords[0], coords[1], coords[2]); vec3.normalize(v, v); return v; } function calculateNormalWithCross(v1, v2, v3) { var dir1 = vec3.create(); vec3.sub(dir1, v2, v1); var dir2 = vec3.create(); vec3.sub(dir2, v3, v1); var cross = vec3.create(); vec3.cross(cross, dir1, dir2); vec3.normalize(cross, cross); return cross; } var tessy = (function initTesselator() { // function called for each vertex of tesselator output function vertexCallback(data, polyVertArray) { polyVertArray[polyVertArray.length] = data[0]; polyVertArray[polyVertArray.length] = data[1]; polyVertArray[polyVertArray.length] = data[2]; } function begincallback(type) { } function errorcallback(errno) { } // callback for when segments intersect and must be split function combinecallback(coords, data, weight) { return [coords[0], coords[1], coords[2]]; } function edgeCallback(flag) { // don't really care about the flag, but need no-strip/no-fan behavior } var tessy = new libtess.GluTesselator(); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); return tessy; })();