diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..f190e4bc19841cf1aa289fc09f5b260ea70f0b82
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,274 @@
+
+# Created by https://www.gitignore.io/api/java,maven,macos,linux,eclipse,windows,netbeans,intellij
+# Edit at https://www.gitignore.io/?templates=java,maven,macos,linux,eclipse,windows,netbeans,intellij
+
+
+# User specific
+.sonarlint/
+Servers/
+
+### Eclipse ###
+
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+.recommenders
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# PyDev specific (Python IDE for Eclipse)
+*.pydevproject
+
+# CDT-specific (C/C++ Development Tooling)
+.cproject
+
+# CDT- autotools
+.autotools
+
+# Java annotation processor (APT)
+.factorypath
+
+# PDT-specific (PHP Development Tools)
+.buildpath
+
+# sbteclipse plugin
+.target
+
+# Tern plugin
+.tern-project
+
+# TeXlipse plugin
+.texlipse
+
+# STS (Spring Tool Suite)
+.springBeans
+
+# Code Recommenders
+.recommenders/
+
+# Annotation Processing
+.apt_generated/
+
+# Scala IDE specific (Scala & Java development for Eclipse)
+.cache-main
+.scala_dependencies
+.worksheet
+
+### Eclipse Patch ###
+# Eclipse Core
+.project
+
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+# Annotation Processing
+.apt_generated
+
+.sts4-cache/
+
+### Intellij ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn.  Uncomment if using
+# auto-import.
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Intellij Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+.idea/sonarlint
+
+### Java ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Maven ###
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+
+### NetBeans ###
+**/nbproject/private/
+**/nbproject/Makefile-*.mk
+**/nbproject/Package-*.bash
+build/
+nbbuild/
+dist/
+nbdist/
+.nb-gradle/
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.gitignore.io/api/java,maven,macos,linux,eclipse,windows,netbeans,intellij
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..d645695673349e3947e8e5ae42332d0ac3164cd7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/README.md b/README.md
index 1019e93d258ef46be26629bcee0ed5ddb996f31d..ba34ed7d590f6acfb5e6e0ba204428e1a349e537 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,11 @@
 # CityGMLViewer
 
-A CityGMLViewer based on OpenGL. Can display CityGML 2 and 3
\ No newline at end of file
+A CityGMLViewer based on OpenGL. Can display CityGML 2 and 3
+
+## Installation
+
+Use [Maven](https://maven.apache.org/) to build the viewer.
+
+```bash
+mvn package
+```
diff --git a/color.properties b/color.properties
new file mode 100644
index 0000000000000000000000000000000000000000..6e1a6bc11884ae8fe0b713881b61c00d4740442d
--- /dev/null
+++ b/color.properties
@@ -0,0 +1,10 @@
+groundColor=0.9411765 0.9019608 0.54901963
+roofColor=1 0 0
+doorColor=1 0.784313 0
+windowColor=0.0 0.5019608 0.5019608
+wallColor=1 1 1
+bridgeColor=1 0.49803922 0.3137255
+landColor=0.64705884 0.16470589 0.16470589
+transportationColor=1 1 0
+vegetationColor=0.5647059 0.93333334 0.5647059
+waterColor=0.5294118 0.80784315 0.98039216
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3ebaf0b52b56aa65608dba63fda646067c661f7a
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,193 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<groupId>de.hft.stuttgart</groupId>
+	<artifactId>citygml-viewer-lwjgl</artifactId>
+	<version>0.0.1</version>
+
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+		<maven.compiler.source>1.8</maven.compiler.source>
+		<maven.compiler.target>1.8</maven.compiler.target>
+		<lwjgl.version>3.2.3</lwjgl.version>
+		<joml.version>1.10.1</joml.version>
+	</properties>
+
+	<profiles>
+		<profile>
+			<id>lwjgl-natives-linux-amd64</id>
+			<activation>
+				<os>
+					<family>unix</family>
+					<arch>amd64</arch>
+				</os>
+			</activation>
+			<properties>
+				<lwjgl.natives>natives-linux</lwjgl.natives>
+			</properties>
+		</profile>
+		<profile>
+			<id>lwjgl-natives-macos-amd64</id>
+			<activation>
+				<os>
+					<family>mac</family>
+					<arch>amd64</arch>
+				</os>
+			</activation>
+			<properties>
+				<lwjgl.natives>natives-macos</lwjgl.natives>
+			</properties>
+		</profile>
+		<profile>
+			<id>lwjgl-natives-windows-amd64</id>
+			<activation>
+				<os>
+					<family>windows</family>
+					<arch>amd64</arch>
+				</os>
+			</activation>
+			<properties>
+				<lwjgl.natives>natives-windows</lwjgl.natives>
+			</properties>
+		</profile>
+	</profiles>
+
+	<dependencyManagement>
+		<dependencies>
+			<dependency>
+				<groupId>org.lwjgl</groupId>
+				<artifactId>lwjgl-bom</artifactId>
+				<version>${lwjgl.version}</version>
+				<scope>import</scope>
+				<type>pom</type>
+			</dependency>
+		</dependencies>
+	</dependencyManagement>
+
+	<dependencies>
+		<!-- https://mvnrepository.com/artifact/org.citygml4j/citygml4j -->
+		<dependency>
+			<groupId>org.citygml4j</groupId>
+			<artifactId>citygml4j</artifactId>
+			<version>3.0.0-rc.2</version>
+		</dependency>
+		<!-- https://mvnrepository.com/artifact/org.locationtech.proj4j/proj4j -->
+		<dependency>
+			<groupId>org.locationtech.proj4j</groupId>
+			<artifactId>proj4j</artifactId>
+			<version>1.1.3</version>
+		</dependency>
+		<dependency>
+			<groupId>org.lwjgl</groupId>
+			<artifactId>lwjgl</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.lwjgl</groupId>
+			<artifactId>lwjgl-glfw</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.lwjgl</groupId>
+			<artifactId>lwjgl-opengl</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.lwjgl</groupId>
+			<artifactId>lwjgl-nfd</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.lwjgl</groupId>
+			<artifactId>lwjgl</artifactId>
+			<classifier>natives-windows</classifier>
+		</dependency>
+		<dependency>
+			<groupId>org.lwjgl</groupId>
+			<artifactId>lwjgl-glfw</artifactId>
+			<classifier>natives-windows</classifier>
+		</dependency>
+		<dependency>
+			<groupId>org.lwjgl</groupId>
+			<artifactId>lwjgl-opengl</artifactId>
+			<classifier>natives-windows</classifier>
+		</dependency>
+		<dependency>
+			<groupId>org.lwjgl</groupId>
+			<artifactId>lwjgl</artifactId>
+			<classifier>natives-linux</classifier>
+		</dependency>
+		<dependency>
+			<groupId>org.lwjgl</groupId>
+			<artifactId>lwjgl-glfw</artifactId>
+			<classifier>natives-linux</classifier>
+		</dependency>
+		<dependency>
+			<groupId>org.lwjgl</groupId>
+			<artifactId>lwjgl-opengl</artifactId>
+			<classifier>natives-linux</classifier>
+		</dependency>
+		<dependency>
+			<groupId>org.lwjgl</groupId>
+			<artifactId>lwjgl-nfd</artifactId>
+			<classifier>natives-linux</classifier>
+		</dependency>
+		<dependency>
+			<groupId>org.lwjgl</groupId>
+			<artifactId>lwjgl-nfd</artifactId>
+			<classifier>natives-windows</classifier>
+		</dependency>
+		<dependency>
+			<groupId>org.joml</groupId>
+			<artifactId>joml</artifactId>
+			<version>${joml.version}</version>
+		</dependency>
+		<!-- https://mvnrepository.com/artifact/org.jogamp.jogl/jogl-all-main -->
+		<dependency>
+			<groupId>org.jogamp.jogl</groupId>
+			<artifactId>jogl-all-main</artifactId>
+			<version>2.3.2</version>
+		</dependency>
+		<!-- https://mvnrepository.com/artifact/org.jogamp.gluegen/gluegen-rt-main -->
+		<dependency>
+			<groupId>org.jogamp.gluegen</groupId>
+			<artifactId>gluegen-rt-main</artifactId>
+			<version>2.3.2</version>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+				<version>3.2.0</version>
+				<configuration>
+					<archive>
+						<manifest>
+							<classpathPrefix>libs/</classpathPrefix>
+							<mainClass>de.hft.stuttgart.citygml.viewer.CityGMLViewer</mainClass>
+							<addClasspath>true</addClasspath>
+						</manifest>
+					</archive>
+				</configuration>
+			</plugin>
+			<plugin>
+				<artifactId>maven-assembly-plugin</artifactId>
+				<configuration>
+					<appendAssemblyId>false</appendAssemblyId>
+					<finalName>${project.artifactId}-${project.version}</finalName>
+					<descriptors>
+						<descriptor>${project.basedir}/src/assembly/assembly.xml</descriptor>
+					</descriptors>
+				</configuration>
+				<executions>
+					<execution>
+						<id>create-archive</id>
+						<phase>package</phase>
+						<goals>
+							<goal>single</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>
\ No newline at end of file
diff --git a/src/assembly/assembly.xml b/src/assembly/assembly.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5eb2d19351d18e9f7101966f5a9d62c35c22a7fd
--- /dev/null
+++ b/src/assembly/assembly.xml
@@ -0,0 +1,36 @@
+<assembly
+	xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
+	<id>zip</id>
+	<formats>
+		<format>zip</format>
+	</formats>
+	<includeBaseDirectory>false</includeBaseDirectory>
+	<dependencySets>
+		<dependencySet>
+			<outputDirectory>libs</outputDirectory>
+			<excludes>
+				<exclude>${project.groupId}:${project.artifactId}:jar:*</exclude>
+			</excludes>
+		</dependencySet>
+	</dependencySets>
+	<fileSets>
+		<fileSet>
+			<directory>${project.build.directory}</directory>
+			<outputDirectory>/</outputDirectory>
+			<includes>
+				<include>${project.artifactId}-${project.version}.jar</include>
+			</includes>
+		</fileSet>
+		<fileSet>
+			<directory>${project.basedir}/src/assembly</directory>
+			<outputDirectory>/</outputDirectory>
+			<includes>
+				<include>start.bat</include>
+				<include>color.properties</include>
+			</includes>
+			<filtered>true</filtered>
+		</fileSet>
+	</fileSets>
+</assembly>
\ No newline at end of file
diff --git a/src/assembly/color.properties b/src/assembly/color.properties
new file mode 100644
index 0000000000000000000000000000000000000000..6e1a6bc11884ae8fe0b713881b61c00d4740442d
--- /dev/null
+++ b/src/assembly/color.properties
@@ -0,0 +1,10 @@
+groundColor=0.9411765 0.9019608 0.54901963
+roofColor=1 0 0
+doorColor=1 0.784313 0
+windowColor=0.0 0.5019608 0.5019608
+wallColor=1 1 1
+bridgeColor=1 0.49803922 0.3137255
+landColor=0.64705884 0.16470589 0.16470589
+transportationColor=1 1 0
+vegetationColor=0.5647059 0.93333334 0.5647059
+waterColor=0.5294118 0.80784315 0.98039216
\ No newline at end of file
diff --git a/src/assembly/start.bat b/src/assembly/start.bat
new file mode 100644
index 0000000000000000000000000000000000000000..b932956b7c16fbf5dc718152b0039ac7b60ac814
--- /dev/null
+++ b/src/assembly/start.bat
@@ -0,0 +1 @@
+start javaw -jar ${project.artifactId}-${project.version}.jar
\ No newline at end of file
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/Camera.java b/src/main/java/de/hft/stuttgart/citygml/viewer/Camera.java
new file mode 100644
index 0000000000000000000000000000000000000000..8c5a80bf0161cce5b4134f4ee4d404474faa5dec
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/Camera.java
@@ -0,0 +1,127 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer;
+
+import java.nio.FloatBuffer;
+
+import org.joml.Math;
+import org.joml.Matrix4f;
+import org.joml.Vector3f;
+import org.lwjgl.system.MemoryUtil;
+
+public class Camera {
+
+	private static final float MIN_DISTANCE = 1f;
+	private static final float PI = (float) Math.PI;
+
+	private Matrix4f projMatrix;
+	private Matrix4f viewMatrix;
+	private Matrix4f projViewMatrix;
+
+	private float rotateAroundX = 2f;
+	private float rotateAroundZ = 3f;
+
+	private Vector3f eyeVec = new Vector3f();
+	private Vector3f center = new Vector3f();
+	private Vector3f up = new Vector3f();
+	private float distance = 5;
+
+	private static final float SCROLL_PERCENTAGE = 0.1f;
+
+	private UniformLocation uniformLocation;
+	private FloatBuffer matrixBuffer = MemoryUtil.memAllocFloat(16);
+
+	private boolean isOrtho = false;
+
+	public Camera(Shader shader) {
+		uniformLocation = shader.getUniformLocation("projViewModel");
+		projViewMatrix = new Matrix4f();
+		viewMatrix = new Matrix4f();
+		projMatrix = new Matrix4f();
+		up.z = 1;
+	}
+
+	public void reshape(int width, int height, float distance) {
+		if (isOrtho) {
+			projMatrix.setOrtho(0, width, height, 0, 0, distance);
+		} else {
+			// 90 degree fow
+			float fow = PI / 2f;
+			float aspectRatio = (float) width / height;
+			projMatrix.setPerspective(fow, aspectRatio, MIN_DISTANCE, distance);
+
+		}
+		updateMatrix();
+	}
+
+	public void setOrtho(boolean isOrtho) {
+		this.isOrtho = isOrtho;
+	}
+
+	private void updateMatrix() {
+		// view matrix
+		if (isOrtho) {
+			projMatrix.get(matrixBuffer);
+		} else {
+			float sx = Math.sin(rotateAroundX);
+			float cx = Math.cos(rotateAroundX);
+			float sy = Math.sin(rotateAroundZ);
+			float cy = Math.cos(rotateAroundZ);
+			float sxDistance = distance * sx;
+			eyeVec.x = sxDistance * cy;
+			eyeVec.y = sxDistance * sy;
+			eyeVec.z = distance * cx;
+			viewMatrix.identity();
+			viewMatrix.translate(0, 0, distance);
+			viewMatrix.lookAt(eyeVec, center, up);
+			projMatrix.mul(viewMatrix, projViewMatrix);
+			// model matrix is identity
+			projViewMatrix.get(matrixBuffer);
+
+		}
+		uniformLocation.uploadMat4(matrixBuffer);
+	}
+
+	public void drag(double dragDiffX, double dragDiffY) {
+		rotateAroundX += dragDiffY / 500f;
+		rotateAroundZ -= dragDiffX / 500f;
+		if (rotateAroundX > Math.PI) {
+			rotateAroundX = (float) Math.PI - 0.001f;
+		} else if (rotateAroundX < 0.001) {
+			rotateAroundX = (float) 0.001;
+		}
+		updateMatrix();
+	}
+
+	public void setDistance(float distance) {
+		this.distance = distance;
+		updateMatrix();
+	}
+
+	public void scroll(int scroll) {
+		int addedDistance = (int) (distance * scroll * SCROLL_PERCENTAGE);
+		distance += addedDistance;
+		updateMatrix();
+	}
+
+	public void free() {
+		MemoryUtil.memFree(matrixBuffer);
+	}
+
+	public void move(double dragDiffX, double dragDiffY) {
+	}
+
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/CityGMLViewer.java b/src/main/java/de/hft/stuttgart/citygml/viewer/CityGMLViewer.java
new file mode 100644
index 0000000000000000000000000000000000000000..eb9fae20cad1be54161b8ab0ea2cecaebc843023
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/CityGMLViewer.java
@@ -0,0 +1,415 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer;
+
+import java.awt.geom.AffineTransform;
+import java.awt.image.AffineTransformOp;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.DoubleBuffer;
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.imageio.ImageIO;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.citygml4j.CityGMLContext;
+import org.citygml4j.CityGMLContextException;
+import org.citygml4j.model.core.AbstractCityObject;
+import org.citygml4j.model.core.AbstractFeature;
+import org.citygml4j.xml.reader.ChunkingOptions;
+import org.citygml4j.xml.reader.CityGMLInputFactory;
+import org.citygml4j.xml.reader.CityGMLReadException;
+import org.citygml4j.xml.reader.CityGMLReader;
+import org.lwjgl.PointerBuffer;
+import org.lwjgl.glfw.Callbacks;
+import org.lwjgl.glfw.GLFW;
+import org.lwjgl.glfw.GLFWErrorCallback;
+import org.lwjgl.glfw.GLFWMouseButtonCallback;
+import org.lwjgl.glfw.GLFWScrollCallback;
+import org.lwjgl.glfw.GLFWWindowSizeCallbackI;
+import org.lwjgl.opengl.GL;
+import org.lwjgl.opengl.GL11;
+import org.lwjgl.opengl.GL13;
+import org.lwjgl.opengl.GL30;
+import org.lwjgl.system.MemoryStack;
+import org.lwjgl.system.MemoryUtil;
+import org.lwjgl.util.nfd.NativeFileDialog;
+import org.xml.sax.SAXException;
+
+import com.jogamp.opengl.GLException;
+
+import de.hft.stuttgart.citygml.viewer.datastructure.BoundingBox;
+import de.hft.stuttgart.citygml.viewer.math.Vector3d;
+import de.hft.stuttgart.citygml.viewer.parser.CityGMLParser;
+import de.hft.stuttgart.citygml.viewer.parser.FeatureMapper;
+import de.hft.stuttgart.citygml.viewer.parser.ObservedInputStream;
+import de.hft.stuttgart.citygml.viewer.parser.ParserConfiguration;
+
+public class CityGMLViewer {
+
+	private static final Logger log = Logger.getLogger(CityGMLViewer.class.getName());
+
+	private static Camera camera;
+
+	private static boolean mouse1Down = false;
+	private static boolean mouse2Down = false;
+
+	private static double x;
+	private static double y;
+
+	private static int width = 1024;
+	private static int height = 768;
+
+	private static List<PolygonViewInformation> viewing = new ArrayList<>();
+	private static float cameraViewDistance = 10000f;
+
+	private static long windowId;
+
+	private static ProgressBar bar;
+
+	public static void main(String[] args) {
+		File f = null;
+		if (args != null && args.length == 1) {
+			f = new File(args[0]);
+			if (!f.exists() || f.isDirectory()) {
+				// no file given
+				f = null;
+			}
+		}
+		if (f == null) {
+			f = showFileChooserDialog();
+		}
+		try {
+			setupWindow(f);
+		} finally {
+			for (PolygonViewInformation view : viewing) {
+				view.destroy();
+			}
+		}
+	}
+
+	private static File showFileChooserDialog() {
+		File f = null;
+		try (MemoryStack stack = MemoryStack.stackPush()) {
+			File defaultPath = new File("bla");
+			PointerBuffer pathPointer = stack.mallocPointer(1);
+			int success = NativeFileDialog.NFD_OpenDialog("gml,xml", defaultPath.getAbsolutePath(), pathPointer);
+			if (success == NativeFileDialog.NFD_OKAY) {
+				String filePath = pathPointer.getStringUTF8(0);
+				f = new File(filePath);
+			} else {
+				System.exit(0);
+			}
+		}
+		return f;
+	}
+
+	private static void screenShot() {
+		// Creating an rbg array of total pixels
+		int[] pixels = new int[width * height];
+		int bindex;
+		// allocate space for RBG pixels
+		ByteBuffer fb = ByteBuffer.allocateDirect(width * height * 3);
+
+		// grab a copy of the current frame contents as RGB
+		GL11.glReadPixels(0, 0, width, height, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, fb);
+
+		BufferedImage imageIn = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+		// convert RGB data in ByteBuffer to integer array
+		for (int i = 0; i < pixels.length; i++) {
+			bindex = i * 3;
+			pixels[i] = (fb.get(bindex) << 16) + (fb.get(bindex + 1) << 8) + (fb.get(bindex + 2));
+		}
+		// Allocate colored pixel to buffered Image
+		imageIn.setRGB(0, 0, width, height, pixels, 0, width);
+
+		// Creating the transformation direction (horizontal)
+		AffineTransform at = AffineTransform.getScaleInstance(1, -1);
+		at.translate(0, -imageIn.getHeight(null));
+
+		// Applying transformation
+		AffineTransformOp opRotated = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
+		BufferedImage imageOut = opRotated.filter(imageIn, null);
+
+		try {
+			int nr = 0;
+			File f = new File("screen_" + nr + ".png");
+			while (f.exists()) {
+				nr++;
+				f = new File("screen_" + nr + ".png");
+			}
+			ImageIO.write(imageOut, "png", f);
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
+
+	private static void setupWindow(File f) {
+		GLFW.glfwSetErrorCallback(GLFWErrorCallback.createPrint(System.err));
+		if (!GLFW.glfwInit()) {
+			throw new IllegalStateException("Unable to initialize GLFW");
+		}
+		String title = "CityGMLViewer";
+
+		GLFW.glfwDefaultWindowHints(); // Loads GLFW's default window settings
+		GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); // Sets window to be visible
+		GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE); // Sets whether the window is resizable
+		GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE);
+		GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3);
+		GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3);
+		GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, 2);
+
+		windowId = GLFW.glfwCreateWindow(width, height, title, 0, 0);
+		if (windowId == 0) {
+			GLFW.glfwTerminate();
+			throw new IllegalStateException("Failed to create window");
+		}
+
+		GLFW.glfwSetWindowSizeCallback(windowId, new GLFWWindowSizeCallbackI() {
+
+			@Override
+			public void invoke(long window, int width, int height) {
+				CityGMLViewer.width = width;
+				CityGMLViewer.height = height;
+				if (camera != null) {
+					camera.reshape(width, height, cameraViewDistance);
+				}
+				if (bar != null) {
+					bar.updateWindowSize(width, height);
+				}
+				GL11.glViewport(0, 0, width, height);
+			}
+		});
+
+		GLFW.glfwSetKeyCallback(windowId, (window, key, scancode, action, mods) -> {
+			if (key == GLFW.GLFW_KEY_ESCAPE && action == GLFW.GLFW_PRESS) {
+				GLFW.glfwSetWindowShouldClose(window, true);
+			} else if (key == GLFW.GLFW_KEY_F9 && action == GLFW.GLFW_PRESS) {
+				screenShot();
+			}
+		});
+
+		GLFW.glfwSetMouseButtonCallback(windowId, new GLFWMouseButtonCallback() {
+
+			@Override
+			public void invoke(long window, int button, int action, int mods) {
+				if (button == GLFW.GLFW_MOUSE_BUTTON_1 && action == GLFW.GLFW_PRESS) {
+					mouse1Down = true;
+					try (MemoryStack stack = MemoryStack.stackPush()) {
+						DoubleBuffer xBuffer = stack.mallocDouble(1);
+						DoubleBuffer yBuffer = stack.mallocDouble(1);
+						GLFW.glfwGetCursorPos(windowId, xBuffer, yBuffer);
+						x = xBuffer.get(0);
+						y = yBuffer.get(0);
+					}
+				} else if (button == GLFW.GLFW_MOUSE_BUTTON_1 && action == GLFW.GLFW_RELEASE) {
+					mouse1Down = false;
+				} else if (button == GLFW.GLFW_MOUSE_BUTTON_2 && action == GLFW.GLFW_PRESS) {
+					mouse2Down = true;
+					try (MemoryStack stack = MemoryStack.stackPush()) {
+						DoubleBuffer xBuffer = stack.mallocDouble(1);
+						DoubleBuffer yBuffer = stack.mallocDouble(1);
+						GLFW.glfwGetCursorPos(windowId, xBuffer, yBuffer);
+						x = xBuffer.get(0);
+						y = yBuffer.get(0);
+					}
+				} else if (button == GLFW.GLFW_MOUSE_BUTTON_2 && action == GLFW.GLFW_RELEASE) {
+					mouse2Down = false;
+				}
+			}
+		});
+
+		GLFW.glfwSetScrollCallback(windowId, new GLFWScrollCallback() {
+
+			@Override
+			public void invoke(long window, double xoffset, double yoffset) {
+				camera.scroll((int) (-yoffset));
+			}
+		});
+
+		GLFW.glfwSetCursorPosCallback(windowId, (window, xPos, yPos) -> {
+			if (mouse1Down) {
+				try (MemoryStack stack = MemoryStack.stackPush()) {
+					double dragDiffX = xPos - x;
+					double dragDiffY = yPos - y;
+					camera.drag(dragDiffX, dragDiffY);
+					x = xPos;
+					y = yPos;
+				}
+			} else if (mouse2Down) {
+				try (MemoryStack stack = MemoryStack.stackPush()) {
+					double dragDiffX = xPos - x;
+					double dragDiffY = yPos - y;
+					camera.move(dragDiffX, dragDiffY);
+					x = xPos;
+					y = yPos;
+				}
+			}
+		});
+
+		GLFW.glfwMakeContextCurrent(windowId); // glfwSwapInterval needs a context on the calling thread, otherwise will
+												// cause
+		// NO_CURRENT_CONTEXT error
+		org.lwjgl.opengl.GLCapabilities caps = GL.createCapabilities(); // Will let lwjgl know we want to use this
+																		// context as the context to draw with
+		if (!caps.OpenGL33) {
+			log.warning("OpenGL 33 is not supported");
+		}
+		GLFW.glfwSwapInterval(1); // How many draws to swap the buffer
+
+		GL11.glEnable(GL11.GL_DEPTH_TEST);
+		GL11.glEnable(GL13.GL_MULTISAMPLE);
+		GL11.glDisable(GL11.GL_CULL_FACE);
+
+		GL11.glViewport(0, 0, width, height);
+		GLFW.glfwShowWindow(windowId); // Shows the window
+
+		if (log.isLoggable(Level.INFO)) {
+			log.info("OpenGL error code after displaying the window: " + GL11.glGetError());
+		}
+
+		FloatBuffer clearColor = null;
+		FloatBuffer clearDepth = null;
+
+		try {
+			Shader program = new Shader("vertex.vert", "fragment.frag");
+			program.use();
+			if (log.isLoggable(Level.INFO)) {
+				log.info("OpenGL error code after shader loading: " + GL11.glGetError());
+			}
+			camera = new Camera(program);
+			camera.setOrtho(true);
+			camera.reshape(width, height, cameraViewDistance);
+			bar = new ProgressBar(windowId, width, height);
+			bar.drawProgress(0);
+			if (log.isLoggable(Level.INFO)) {
+				log.info("OpenGL error code after drawing initial progress bar: " + GL11.glGetError());
+			}
+
+			loadGmlFile(f);
+			bar.free();
+			bar = null;
+			if (log.isLoggable(Level.INFO)) {
+				log.info("OpenGL error code after loading GML file: " + GL11.glGetError());
+			}
+			camera.setOrtho(false);
+			camera.reshape(width, height, cameraViewDistance);
+
+			clearColor = MemoryUtil.memAllocFloat(4);
+			clearColor.put(0, 0.9411765f).put(1, 1f).put(2, 1f).put(3, 1f);
+			clearDepth = MemoryUtil.memAllocFloat(1);
+			clearDepth.put(0, 1f);
+
+			if (log.isLoggable(Level.INFO)) {
+				log.info("OpenGL error code before loop: " + GL11.glGetError());
+			}
+			while (!GLFW.glfwWindowShouldClose(windowId)) {
+				/* Do something */
+				GL30.glClearBufferfv(GL11.GL_COLOR, 0, clearColor);
+				GL30.glClearBufferfv(GL11.GL_DEPTH, 0, clearDepth);
+
+				for (PolygonViewInformation view : viewing) {
+					view.draw();
+				}
+
+				Thread.sleep(15);
+
+				GLFW.glfwSwapBuffers(windowId);
+				GLFW.glfwPollEvents();
+			}
+		} catch (InterruptedException e) {
+			e.printStackTrace();
+			Thread.currentThread().interrupt();
+		} finally {
+			MemoryUtil.memFree(clearColor);
+			MemoryUtil.memFree(clearDepth);
+			if (camera != null) {
+				camera.free();
+			}
+		}
+
+		GLFW.glfwDestroyWindow(windowId);
+		Callbacks.glfwFreeCallbacks(windowId);
+		GLFW.glfwTerminate();
+	}
+
+	private static void loadGmlFile(File f) {
+		try {
+			File file = f;
+			ParserConfiguration config = new ParserConfiguration();
+			CityGMLParser.parseEpsgCodeFromFile(file, config);
+			CityGMLContext context = CityGMLContext.newInstance();
+			CityGMLInputFactory in = context.createCityGMLInputFactory()
+					.withChunking(ChunkingOptions.chunkByCityModelMembers());
+			try (ObservedInputStream ois = new ObservedInputStream(file);
+					CityGMLReader reader = in.createCityGMLReader(file.getAbsolutePath(), ois)) {
+				ois.addListener(p -> bar.drawProgress((int) (p * 82)));
+				FeatureMapper mapper = new FeatureMapper(config);
+				while (reader.hasNext()) {
+					AbstractFeature chunk = reader.next();
+					if (chunk instanceof AbstractCityObject) {
+						AbstractCityObject aco = (AbstractCityObject) chunk;
+						aco.accept(mapper);
+					}
+				}
+				BoundingBox bbox = BoundingBox.of(mapper);
+				bar.drawProgress(81);
+				Vector3d center = bbox.getCenter();
+				mapper.movePolygonsBy(center);
+				bar.drawProgress(82);
+				long nrOfPolygons = (long) mapper.getLod1Polygons().size() + mapper.getLod2Polygons().size()
+						+ mapper.getLod3Polygons().size() + mapper.getLod4Polygons().size();
+				long[] count = new long[1];
+				PolygonListener l = () -> {
+					count[0]++;
+					bar.drawProgress(82 + (int) (count[0] * 9 / nrOfPolygons));
+				};
+				if (!mapper.getLod1Polygons().isEmpty()) {
+					PolygonViewInformation lod1ViewInfo = new PolygonViewInformation(mapper.getLod1Polygons(), l);
+					viewing.add(lod1ViewInfo);
+				}
+				if (!mapper.getLod2Polygons().isEmpty()) {
+					PolygonViewInformation lod2ViewInfo = new PolygonViewInformation(mapper.getLod2Polygons(), l);
+					viewing.add(lod2ViewInfo);
+				}
+				if (!mapper.getLod3Polygons().isEmpty()) {
+					PolygonViewInformation lod3ViewInfo = new PolygonViewInformation(mapper.getLod3Polygons(), l);
+					viewing.add(lod3ViewInfo);
+				}
+				if (!mapper.getLod3Polygons().isEmpty()) {
+					PolygonViewInformation lod4ViewInfo = new PolygonViewInformation(mapper.getLod4Polygons(), l);
+					viewing.add(lod4ViewInfo);
+				}
+
+				double longestSide = bbox.getDiagonalLength() * 0.2;
+				double d = longestSide / Math.tan(Math.toRadians(30) / 2);
+				double translateZ = -d;
+				camera.setDistance((float) translateZ);
+				cameraViewDistance = (float) longestSide * 20f;
+				camera.reshape(width, height, cameraViewDistance);
+			}
+		} catch (GLException | IOException | ParserConfigurationException | SAXException | CityGMLContextException
+				| CityGMLReadException e) {
+			e.printStackTrace();
+		}
+	}
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/CityGmlHandler.java b/src/main/java/de/hft/stuttgart/citygml/viewer/CityGmlHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..4377dd4b3308b79fdb0f0683838fc2e08d8f4d69
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/CityGmlHandler.java
@@ -0,0 +1,82 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import de.hft.stuttgart.citygml.viewer.math.Vector3d;
+import de.hft.stuttgart.citygml.viewer.parser.EnvelopeFoundException;
+
+public class CityGmlHandler extends DefaultHandler {
+
+	private boolean foundEnvelope = false;
+
+	private Vector3d lowerCorner;
+	private Vector3d upperCorner;
+	private String epsg;
+
+	private boolean nextContentIsLowerCorner = false;
+	private boolean nextContentIsUpperCorner = false;
+
+	@Override
+	public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+		if (foundEnvelope) {
+			if (qName.endsWith("lowerCorner")) {
+				nextContentIsLowerCorner = true;
+			} else if (qName.endsWith("upperCorner")) {
+				nextContentIsUpperCorner = true;
+			}
+		} else if (qName.endsWith("Envelope")) {
+			foundEnvelope = true;
+			epsg = attributes.getValue("srsName");
+		}
+	}
+
+	public Vector3d getLowerCorner() {
+		return lowerCorner;
+	}
+
+	public Vector3d getUpperCorner() {
+		return upperCorner;
+	}
+
+	public String getEpsg() {
+		return epsg;
+	}
+
+	@Override
+	public void characters(char[] ch, int start, int length) throws SAXException {
+		if (nextContentIsLowerCorner) {
+			lowerCorner = parsePoint(ch, start, length);
+			nextContentIsLowerCorner = false;
+		} else if (nextContentIsUpperCorner) {
+			upperCorner = parsePoint(ch, start, length);
+			nextContentIsUpperCorner = false;
+			throw new EnvelopeFoundException("Found envelope, stop parsing");
+		}
+	}
+
+	private Vector3d parsePoint(char[] ch, int start, int length) {
+		String s = new String(ch, start, length);
+		String[] coords = s.split("\\s+");
+		double x = Double.parseDouble(coords[0]);
+		double y = Double.parseDouble(coords[1]);
+		double z = Double.parseDouble(coords[2]);
+		return new Vector3d(x, y, z);
+	}
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/PolygonListener.java b/src/main/java/de/hft/stuttgart/citygml/viewer/PolygonListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..ab0a8095b8515ab610381be2c0bfa130e05afec6
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/PolygonListener.java
@@ -0,0 +1,22 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer;
+
+public interface PolygonListener {
+	
+	public void finishedPolygon();
+
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/PolygonViewInformation.java b/src/main/java/de/hft/stuttgart/citygml/viewer/PolygonViewInformation.java
new file mode 100644
index 0000000000000000000000000000000000000000..15b0e417f67244b5a90a3d020fbd12b8ebda1e34
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/PolygonViewInformation.java
@@ -0,0 +1,157 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer;
+
+import static org.lwjgl.opengl.GL11.GL_FLOAT;
+
+import java.awt.Color;
+import java.nio.Buffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.lwjgl.opengl.GL11;
+import org.lwjgl.opengl.GL15;
+import org.lwjgl.opengl.GL30;
+import org.lwjgl.opengl.GL44;
+import org.lwjgl.opengl.GL45;
+import org.lwjgl.system.MemoryUtil;
+
+import de.hft.stuttgart.citygml.viewer.datastructure.BufferConstants;
+import de.hft.stuttgart.citygml.viewer.datastructure.Polygon;
+import de.hft.stuttgart.citygml.viewer.datastructure.TesselatedPolygon;
+import de.hft.stuttgart.citygml.viewer.datastructure.Triangle3d;
+import de.hft.stuttgart.citygml.viewer.math.Vector3d;
+
+public class PolygonViewInformation {
+	
+	private static final Vector3d AXIS = new Vector3d(19, 0.8, 1.5).normalize();
+
+
+	private int vao;
+	private IntBuffer buffers;
+	private boolean draw = false;
+	private int elements;
+	
+	private PolygonListener listener;
+
+	public PolygonViewInformation(List<Polygon> polygons, PolygonListener listener) {
+		this.listener = listener;
+		vao = GL45.glCreateVertexArrays();
+		GL45.glVertexArrayAttribBinding(vao, BufferConstants.ATTRIBUTE_POSITION, BufferConstants.BINDING_INDEX_0);
+		GL45.glVertexArrayAttribBinding(vao, BufferConstants.ATTRIBUTE_COLOR, BufferConstants.BINDING_INDEX_0);
+
+		GL45.glVertexArrayAttribFormat(vao, BufferConstants.ATTRIBUTE_POSITION, 3, GL_FLOAT, false, 0);
+		GL45.glVertexArrayAttribFormat(vao, BufferConstants.ATTRIBUTE_COLOR, 3, GL_FLOAT, false, 3 * 4);
+
+		GL45.glEnableVertexArrayAttrib(vao, BufferConstants.ATTRIBUTE_POSITION);
+		GL45.glEnableVertexArrayAttrib(vao, BufferConstants.ATTRIBUTE_COLOR);
+
+		buffers = MemoryUtil.memAllocInt(BufferConstants.SIZE);
+		GL45.glCreateBuffers(buffers);
+		GL45.glVertexArrayElementBuffer(vao, buffers.get(BufferConstants.ELEMENT));
+		GL45.glVertexArrayVertexBuffer(vao, BufferConstants.BINDING_INDEX_0, buffers.get(BufferConstants.VERTEX), 0, (3 + 3) * 4);
+		setupBuffers(polygons);
+	}
+	
+	private void setupBuffers(List<Polygon> polys) {
+		List<TesselatedPolygon> tessPolys = new ArrayList<>();
+		for (Polygon poly : polys) {
+			tessPolys.add(TesselatedPolygon.of(poly));
+			listener.finishedPolygon();
+		}
+		
+		int nrOfTriangles = 0;
+		for (TesselatedPolygon tessPoly : tessPolys) {
+			nrOfTriangles += tessPoly.getTriangles().size();
+		}
+
+		// 6 float for 3 vertices per triangle
+		int moreSpace = nrOfTriangles * 3 * 3 * 2;
+		FloatBuffer vertexData = MemoryUtil.memAllocFloat(moreSpace);
+		IntBuffer elementData = MemoryUtil.memAllocInt(nrOfTriangles * 3);
+		float[] rgba = new float[4];
+		for (TesselatedPolygon tessPoly : tessPolys) {
+			Color baseColor = tessPoly.getColor();
+			double cos = tessPoly.getNormal().dot(AXIS);
+			double acos = Math.acos(cos);
+			// normalize to range [0.3, 0.9]
+			acos = acos / Math.PI;
+			acos = acos * 0.6 + 0.3;
+			Color derivedColor = new Color((int) (baseColor.getRed() * acos), (int) (baseColor.getGreen() * acos),
+					(int) (baseColor.getBlue() * acos));
+			derivedColor.getComponents(rgba);
+			for (Triangle3d tri : tessPoly.getTriangles()) {
+				addPoint(vertexData, tri.getP1(), rgba[0], rgba[1], rgba[2]);
+				addPoint(vertexData, tri.getP2(), rgba[0], rgba[1], rgba[2]);
+				addPoint(vertexData, tri.getP3(), rgba[0], rgba[1], rgba[2]);
+			}
+			listener.finishedPolygon();
+		}
+		for (int i = 0; i < elementData.capacity(); i++) {
+			elementData.put(i);
+		}
+		((Buffer) elementData).flip();
+		((Buffer) vertexData).flip();
+
+		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, buffers.get(BufferConstants.VERTEX));
+		GL44.glBufferStorage(GL15.GL_ARRAY_BUFFER, vertexData, 0);
+		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
+
+		GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, buffers.get(BufferConstants.ELEMENT));
+		GL44.glBufferStorage(GL15.GL_ELEMENT_ARRAY_BUFFER, elementData, 0);
+		GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
+		
+		MemoryUtil.memFree(elementData);
+		MemoryUtil.memFree(vertexData);
+
+		draw = true;
+		elements = elementData.capacity();
+	}
+	
+	private static void addPoint(FloatBuffer lod1VertexData, Vector3d point, float red, float green, float blue) {
+		lod1VertexData.put((float) point.getX());
+		lod1VertexData.put((float) point.getY());
+		lod1VertexData.put((float) point.getZ());
+		lod1VertexData.put(red);
+		lod1VertexData.put(green);
+		lod1VertexData.put(blue);
+	}
+	
+	public boolean shouldDraw() {
+		return draw;
+	}
+	
+	public void setShouldDraw(boolean draw) {
+		this.draw = draw;
+	}
+	
+	public int getVao() {
+		return vao;
+	}
+	
+	public void destroy() {
+		MemoryUtil.memFree(buffers);
+	}
+
+	public void draw() {
+		if (draw) {
+			GL30.glBindVertexArray(vao);
+			GL11.glDrawElements(GL11.GL_TRIANGLES, elements, GL11.GL_UNSIGNED_INT, 0);
+		}		
+	}
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/ProgressBar.java b/src/main/java/de/hft/stuttgart/citygml/viewer/ProgressBar.java
new file mode 100644
index 0000000000000000000000000000000000000000..6ee0d19b2c82975d70a33fbd9f920e00c81e46a6
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/ProgressBar.java
@@ -0,0 +1,186 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+
+import org.lwjgl.glfw.GLFW;
+import org.lwjgl.opengl.GL11;
+import org.lwjgl.opengl.GL15;
+import org.lwjgl.opengl.GL20;
+import org.lwjgl.opengl.GL30;
+import org.lwjgl.system.MemoryStack;
+import org.lwjgl.system.MemoryUtil;
+
+public class ProgressBar {
+
+	private int currentProgress;
+
+	private FloatBuffer clearColor;
+	private FloatBuffer clearDepth;
+
+	private int width;
+	private int height;
+
+	private long windowId;
+
+	private FloatBuffer box;
+	private int vao;
+	private int whiteVertexBuffer;
+	private int indexBufferName;
+	private int colorBufferName;
+
+	public ProgressBar(long windowId, int width, int height) {
+		this.windowId = windowId;
+		this.width = width;
+		this.height = height;
+		clearColor = MemoryUtil.memAllocFloat(4);
+		clearColor.put(0, 0f).put(1, 0f).put(2, 0f).put(3, 1f);
+		clearDepth = MemoryUtil.memAllocFloat(1);
+		clearDepth.put(0, 1f);
+		box = MemoryUtil.memAllocFloat(8 * 3);
+	
+		vao = GL30.glGenVertexArrays();
+		GL30.glBindVertexArray(vao);
+		
+		whiteVertexBuffer = GL15.glGenBuffers();
+		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, whiteVertexBuffer);
+		GL20.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 0, 0);
+		GL20.glEnableVertexAttribArray(0);
+		
+		indexBufferName = GL15.glGenBuffers();
+		GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, indexBufferName);
+		try (MemoryStack stack = MemoryStack.stackPush()) {
+			// index 
+			IntBuffer indexBuffer = stack.mallocInt(12);
+			indexBuffer.put(0, 0);
+			indexBuffer.put(1, 1);
+			indexBuffer.put(2, 2);
+			indexBuffer.put(3, 0);
+			indexBuffer.put(4, 2);
+			indexBuffer.put(5, 3);
+			indexBuffer.put(6, 4);
+			indexBuffer.put(7, 5);
+			indexBuffer.put(8, 6);
+			indexBuffer.put(9, 4);
+			indexBuffer.put(10, 6);
+			indexBuffer.put(11, 7);
+			GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indexBuffer, GL15.GL_STATIC_DRAW);
+			
+			// color
+			FloatBuffer color = stack.mallocFloat(8 * 3);
+			for (int i = 0; i < 4 * 3; i++) {
+				color.put(i, 1);
+			}
+			color.put(12, 0);
+			color.put(13, 0);
+			color.put(14, 1);
+			color.put(15, 0);
+			color.put(16, 0);
+			color.put(17, 1);
+			color.put(18, 0);
+			color.put(19, 0);
+			color.put(20, 1);
+			color.put(21, 0);
+			color.put(22, 0);
+			color.put(23, 1);
+			GL20.glEnableVertexAttribArray(1);
+			colorBufferName = GL15.glGenBuffers();
+			GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, colorBufferName);
+			GL15.glBufferData(GL15.GL_ARRAY_BUFFER, color, GL15.GL_STATIC_DRAW);
+			GL20.glVertexAttribPointer(1, 3, GL11.GL_FLOAT, false, 0, 0);
+		}
+		GL30.glBindVertexArray(0);
+		
+	}
+
+	public void updateWindowSize(int width, int height) {
+		this.width = width;
+		this.height = height;
+		forceDraw(currentProgress);
+	}
+
+	private void draw() {
+	}
+
+	public void drawProgress(int progress) {
+		if (progress == currentProgress) {
+			return;
+		}
+		forceDraw(progress);
+	}
+
+	private void forceDraw(int progress) {
+		GL30.glClearBufferfv(GL11.GL_COLOR, 0, clearColor);
+		GL30.glClearBufferfv(GL11.GL_DEPTH, 0, clearDepth);
+
+		float progressX = width / 2f - 150f;
+		float progressY = height / 2f - 30f;
+		float progressWidth = 300;
+		float progressHeight = 60;
+		float okWidth = (progress / 100f) * progressWidth;
+
+		box.put(0, progressX + okWidth);
+		box.put(1, progressY);
+		box.put(2, 0);
+		box.put(3, progressX + progressWidth);
+		box.put(4, progressY);
+		box.put(5, 0);
+
+		box.put(6, progressX + progressWidth);
+		box.put(7, progressY + progressHeight);
+		box.put(8, 0);
+		box.put(9, progressX + okWidth);
+		box.put(10, progressY + progressHeight);
+		box.put(11, 0);
+		
+		box.put(12, progressX);
+		box.put(13, progressY);
+		box.put(14, 0);
+		box.put(15, progressX + okWidth);
+		box.put(16, progressY);
+		box.put(17, 0);
+		box.put(18, progressX + okWidth);
+		box.put(19, progressY + progressHeight);
+		box.put(20, 0);
+		box.put(21, progressX);
+		box.put(22, progressY + progressHeight);
+		box.put(23, 0);
+		
+		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, whiteVertexBuffer);
+		GL15.glBufferData(GL15.GL_ARRAY_BUFFER, box, GL15.GL_STATIC_DRAW);
+
+		GL30.glBindVertexArray(vao);
+		GL11.glDrawElements(GL11.GL_TRIANGLES, 12, GL11.GL_UNSIGNED_INT, 0);
+		GLFW.glfwSwapBuffers(windowId);
+		GLFW.glfwPollEvents();
+		currentProgress = progress;
+
+		draw();
+	}
+
+	public void free() {
+		MemoryUtil.memFree(clearDepth);
+		MemoryUtil.memFree(clearColor);
+		MemoryUtil.memFree(box);
+		GL15.glDeleteBuffers(whiteVertexBuffer);
+		GL15.glDeleteBuffers(indexBufferName);
+		GL15.glDeleteBuffers(colorBufferName);
+		GL30.glDeleteVertexArrays(vao);
+	}
+
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/Shader.java b/src/main/java/de/hft/stuttgart/citygml/viewer/Shader.java
new file mode 100644
index 0000000000000000000000000000000000000000..8432b62becf5850126be8633f881a15a45cba470
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/Shader.java
@@ -0,0 +1,79 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer;
+
+import static org.lwjgl.opengl.GL20.glAttachShader;
+import static org.lwjgl.opengl.GL20.glCompileShader;
+import static org.lwjgl.opengl.GL20.glCreateShader;
+import static org.lwjgl.opengl.GL20.glDeleteShader;
+import static org.lwjgl.opengl.GL20.glLinkProgram;
+import static org.lwjgl.opengl.GL20.glShaderSource;
+import static org.lwjgl.opengl.GL20.glUseProgram;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
+
+import org.lwjgl.opengl.GL20;
+
+public class Shader {
+	
+	private int programId;
+	
+	public Shader(String vertexPath, String fragmentPath) {
+		int vertexShader = glCreateShader(GL20.GL_VERTEX_SHADER);
+		glShaderSource(vertexShader, readFully(vertexPath));
+		glCompileShader(vertexShader);
+		int fragmentShader = glCreateShader(GL20.GL_FRAGMENT_SHADER);
+		glShaderSource(fragmentShader, readFully(fragmentPath));
+		glCompileShader(fragmentShader);
+		
+		programId = GL20.glCreateProgram();
+		glAttachShader(programId, vertexShader);
+		glAttachShader(programId, fragmentShader);
+		glLinkProgram(programId);
+		
+		glDeleteShader(vertexShader);
+		glDeleteShader(fragmentShader);
+	}
+	
+	public void use() {
+		glUseProgram(programId);
+	}
+	
+	public static void unUse() {
+		glUseProgram(0);
+	}
+	
+	private String readFully(String path) {
+		try (BufferedReader br = new BufferedReader(new InputStreamReader(getClass().getClassLoader().getResourceAsStream(path)))) {
+			char[] buffer = new char[1024];
+			StringBuilder builder = new StringBuilder();
+			while (br.read(buffer) != -1) {
+				builder.append(buffer);
+			}
+			return builder.toString();
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		}
+	}
+
+	public UniformLocation getUniformLocation(String uniformName) {
+		return new UniformLocation(GL20.glGetUniformLocation(programId, uniformName));
+	}
+
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/UniformLocation.java b/src/main/java/de/hft/stuttgart/citygml/viewer/UniformLocation.java
new file mode 100644
index 0000000000000000000000000000000000000000..60cc802b90984ffc313b15077b17a0cd6e98daad
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/UniformLocation.java
@@ -0,0 +1,35 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer;
+
+import java.nio.FloatBuffer;
+
+import org.lwjgl.opengl.GL20;
+
+public class UniformLocation {
+	
+	private int location;
+	
+	public UniformLocation(int location) {
+		this.location = location;
+
+	}
+	
+	public void uploadMat4(FloatBuffer buf) {
+		GL20.glUniformMatrix4fv(location, false, buf);
+	}
+
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/BoundingBox.java b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/BoundingBox.java
new file mode 100644
index 0000000000000000000000000000000000000000..ebf1033933d5136e7d6acd37461aae58f0fa5ad0
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/BoundingBox.java
@@ -0,0 +1,165 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.datastructure;
+
+import java.util.Arrays;
+import java.util.List;
+
+import de.hft.stuttgart.citygml.viewer.math.BoundingBoxCalculator;
+import de.hft.stuttgart.citygml.viewer.math.Vector3d;
+import de.hft.stuttgart.citygml.viewer.parser.FeatureMapper;
+
+/**
+ * An axis aligned bounding box represented by its two corners
+ * 
+ * @author Matthias Betz
+ *
+ */
+public class BoundingBox {
+
+	private Vector3d[] bbox;
+
+	/**
+	 * Creates an axis aligned bounding box containing all points of all polygons
+	 * 
+	 * @param polygons containing the points from which the box will be created
+	 * @return the bounding box around all points
+	 */
+	public static BoundingBox of(List<Polygon> polygons) {
+		return BoundingBoxCalculator.calculateBoundingBox(polygons);
+	}
+	
+	public static BoundingBox ofPoints(List<? extends Vector3d> points) {
+		return BoundingBoxCalculator.calculateBoundingBoxFromPoints(points);
+	}
+
+	/**
+	 * Creates a new bounding box with two vectors as corner points.
+	 * 
+	 * @param box the array of length 2 containing both corners
+	 * @return the new bounding box
+	 */
+	public static BoundingBox of(Vector3d[] box) {
+		return new BoundingBox(box);
+	}
+
+	private BoundingBox(Vector3d[] bbox) {
+		if (bbox == null || bbox.length != 2) {
+			throw new IllegalArgumentException("BoundingBox must be an array of the length 2");
+		}
+		this.bbox = bbox;
+	}
+
+	/**
+	 * Calculates the volume of the box
+	 * 
+	 * @return the volume of the box
+	 */
+	public double getVolume() {
+		double length = getDepth();
+		double width = getWidth();
+		double height = getHeight();
+		return height * width * length;
+	}
+
+	/**
+	 * Calculates the center of the bounding box
+	 * 
+	 * @return the center of the bounding box
+	 */
+	public Vector3d getCenter() {
+		return bbox[0].plus(bbox[1].minus(bbox[0]).mult(0.5));
+	}
+
+	/**
+	 * Getter for the corner array
+	 * 
+	 * @return the array containing the corner points
+	 */
+	public Vector3d[] getBox() {
+		return bbox;
+	}
+
+	/**
+	 * Calculates the width of the bounding box
+	 * 
+	 * @return the width of the bounding box
+	 */
+	public double getWidth() {
+		return bbox[1].getY() - bbox[0].getY();
+	}
+
+	/**
+	 * Calculates the height of the bounding box
+	 * 
+	 * @return the height of the bounding box
+	 */
+	public double getHeight() {
+		return bbox[1].getZ() - bbox[0].getZ();
+	}
+
+	/**
+	 * Calculates the depth of the bounding box
+	 * 
+	 * @return the depth of the bounding box
+	 */
+	public double getDepth() {
+		return bbox[1].getX() - bbox[0].getX();
+	}
+
+	/**
+	 * Returns the length of the longest side of the bounding box, ignoring the
+	 * height. Only X and Y axis are considered
+	 * 
+	 * @return the length of the longest side in X or Y direction
+	 */
+	public double getLongestSide() {
+		double width = getWidth();
+		double depth = getDepth();
+		if (width > depth) {
+			return width;
+		} else {
+			return depth;
+		}
+	}
+
+	/**
+	 * Calculates the direction vector from the lower corner to the upper corner
+	 * 
+	 * @return the direction vector from the lower corner to the upper corner
+	 */
+	public Vector3d getDiagonal() {
+		return bbox[1].minus(bbox[0]);
+	}
+
+	/**
+	 * Calculates the distance between the corner points
+	 * 
+	 * @return the distance between the corner points
+	 */
+	public double getDiagonalLength() {
+		return bbox[1].getDistance(bbox[0]);
+	}
+
+	public static BoundingBox of(FeatureMapper mapper) {
+		return BoundingBoxCalculator.calculateBoundingBox(mapper);
+	}
+	
+	@Override
+	public String toString() {
+		return Arrays.toString(bbox);
+	}
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/BufferConstants.java b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/BufferConstants.java
new file mode 100644
index 0000000000000000000000000000000000000000..85b8b188737d91b3135eb74ddd22d9a86cffc517
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/BufferConstants.java
@@ -0,0 +1,33 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.datastructure;
+
+public class BufferConstants {
+	
+	private BufferConstants() {
+	}
+	
+	public static final int VERTEX = 0;
+	public static final int ELEMENT = 1;
+	public static final int SIZE = 2;
+	
+	public static final int ATTRIBUTE_POSITION = 0;
+	public static final int ATTRIBUTE_COLOR = 1;
+	
+	public static final int UNIFORM_0 = 1;
+	
+	public static final int BINDING_INDEX_0 = 0;
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/JoglTesselator.java b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/JoglTesselator.java
new file mode 100644
index 0000000000000000000000000000000000000000..949b152cc011f31a239491a23c91499fad05d9b3
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/JoglTesselator.java
@@ -0,0 +1,142 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.datastructure;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import com.jogamp.opengl.GL;
+import com.jogamp.opengl.glu.GLU;
+import com.jogamp.opengl.glu.GLUtessellator;
+import com.jogamp.opengl.glu.GLUtessellatorCallbackAdapter;
+
+import de.hft.stuttgart.citygml.viewer.math.Vector3d;
+
+/**
+ * Tesselator to create triangles out of polygons.
+ * 
+ * @author Matthias Betz
+ *
+ */
+public class JoglTesselator {
+
+	private static final String DATA_WAS_NOT_A_LIST = "data was not a list";
+	private static final GLU glu = new GLU();
+	private static final GLUtessellator tess = GLU.gluNewTess();
+	private static final GLUtessellatorCallbackAdapter callback;
+	private static final ArrayList<Primitive> primitives = new ArrayList<>();
+
+	static {
+		callback = new GLUtessellatorCallbackAdapter() {
+
+			private Primitive lastPrimitive;
+
+			@Override
+			public void beginData(int type, Object data) {
+				if (!(data instanceof ArrayList<?>)) {
+					throw new IllegalStateException(DATA_WAS_NOT_A_LIST);
+				}
+				@SuppressWarnings("unchecked")
+				ArrayList<Vector3d> vertices = (ArrayList<Vector3d>) data;
+				lastPrimitive = new Primitive(type, vertices);
+				primitives.add(lastPrimitive);
+			}
+
+			@Override
+			public void vertexData(Object vertex, Object data) {
+				if (!(data instanceof ArrayList)) {
+					throw new IllegalStateException(DATA_WAS_NOT_A_LIST);
+				}
+				if (!(vertex instanceof Integer)) {
+					throw new IllegalStateException("vertex has to be an Integer");
+				}
+				lastPrimitive.addIndex((Integer) vertex);
+			}
+
+			@Override
+			public void combineData(double[] coords, Object[] vertexIndices, float[] weights, Object[] outgoingData,
+					Object data) {
+				if (!(data instanceof ArrayList<?>)) {
+					throw new IllegalStateException(DATA_WAS_NOT_A_LIST);
+				}
+				@SuppressWarnings("unchecked")
+				ArrayList<Vector3d> vertices = (ArrayList<Vector3d>) data;
+				// to ensure the coordinates are valid
+				double[] myCoords = Arrays.copyOf(coords, 3);
+				Vector3d newVertex = new Vector3d(myCoords);
+				outgoingData[0] = vertices.size();
+				vertices.add(newVertex);
+			}
+
+			@Override
+			public void errorData(int errnum, Object data) {
+				throw new TesselationException("GLU Error " + glu.gluErrorString(errnum));
+			}
+
+		};
+		GLU.gluTessCallback(tess, GLU.GLU_TESS_BEGIN_DATA, callback);
+		GLU.gluTessCallback(tess, GLU.GLU_TESS_VERTEX_DATA, callback);
+		GLU.gluTessCallback(tess, GLU.GLU_TESS_ERROR_DATA, callback);
+		GLU.gluTessCallback(tess, GLU.GLU_TESS_COMBINE_DATA, callback);
+		GLU.gluTessCallback(tess, GLU.GLU_TESS_END_DATA, callback);
+	}
+
+	public static TesselatedPolygon tesselatePolygon(Polygon p) {
+		ArrayList<Vector3d> vertices = new ArrayList<>();
+		Vector3d normal = p.calculateNormal();
+		if (p.getExteriorRing().getVertices().isEmpty()) {
+			return new TesselatedPolygon(vertices, Collections.emptyList());
+		}
+		synchronized (tess) {
+			GLU.gluTessProperty(tess, GLU.GLU_TESS_BOUNDARY_ONLY, GL.GL_FALSE);
+			GLU.gluTessBeginPolygon(tess, vertices);
+			GLU.gluTessNormal(tess, normal.getX(), normal.getY(), normal.getZ());
+
+			// exterior ring
+			Ring ext = p.getExteriorRing();
+			createContour(vertices, ext);
+
+			// interior rings
+			for (Ring lr : p.getInteriorRings()) {
+				createContour(vertices, lr);
+			}
+
+			GLU.gluTessEndPolygon(tess);
+			ArrayList<Integer> indices = new ArrayList<>();
+			for (Primitive primitive : primitives) {
+				primitive.fillListWithIndices(indices);
+			}
+			primitives.clear();
+			return new TesselatedPolygon(vertices, indices);
+		}
+	}
+
+	private static void createContour(List<Vector3d> vertices, Ring lr) {
+		GLU.gluTessBeginContour(tess);
+		for (Vector3d v : lr.getVertices()) {
+			vertices.add(v);
+			GLU.gluTessVertex(tess, v.getCoordinates(), 0, vertices.size() - 1);
+		}
+		GLU.gluTessEndContour(tess);
+	}
+
+	private JoglTesselator() {
+		// only static usage
+	}
+
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/Polygon.java b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/Polygon.java
new file mode 100644
index 0000000000000000000000000000000000000000..b3ccf15ec9ca401f12a3cc9b3eb241754cd31388
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/Polygon.java
@@ -0,0 +1,65 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.datastructure;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import de.hft.stuttgart.citygml.viewer.math.Vector3d;
+
+public class Polygon {
+
+	private Ring exteriorRing;
+	private List<Ring> interiorRings;
+	private Color color;
+	
+	public Polygon(Color color) {
+		this.color = color;
+	}
+	
+	public Color getColor() {
+		return color;
+	}
+
+	public Ring getExteriorRing() {
+		return exteriorRing;
+	}
+
+	public void setExteriorRing(Ring exteriorRing) {
+		this.exteriorRing = exteriorRing;
+	}
+
+	public void addInteriorRing(Ring innerRing) {
+		if (interiorRings == null) {
+			interiorRings = new ArrayList<>();
+		}
+		interiorRings.add(innerRing);
+	}
+
+	public List<Ring> getInteriorRings() {
+		if (interiorRings == null) {
+			return Collections.emptyList();
+		}
+		return interiorRings;
+	}
+
+	public Vector3d calculateNormal() {
+		return exteriorRing.calculateNormal();
+	}
+
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/Primitive.java b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/Primitive.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b465185419039bddf5df3f86ce938d9a77066c4
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/Primitive.java
@@ -0,0 +1,103 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.datastructure;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.jogamp.opengl.GL;
+
+import de.hft.stuttgart.citygml.viewer.math.Vector3d;
+
+/**
+ * Result of the tesselation process. Primitives can be converted to triangles.
+ * 
+ * @author Matthias Betz
+ *
+ */
+public class Primitive {
+
+	private static final double AREA_EPSILON = 0.00001;
+
+	private final int type;
+	private final List<Integer> pIndices = new ArrayList<>();
+	private final List<Vector3d> vertices;
+
+	public Primitive(int type, List<Vector3d> vertices) {
+		this.vertices = vertices;
+		this.type = type;
+	}
+
+	public void fillListWithIndices(List<Integer> indices) {
+		switch (type) {
+		case GL.GL_TRIANGLES:
+			for (int i = 0; i < pIndices.size(); i += 3) {
+				addToListIfAreaNonNull(pIndices.get(i), pIndices.get(i + 1), pIndices.get(i + 2), indices);
+			}
+			break;
+		case GL.GL_TRIANGLE_STRIP:
+			for (int i = 0; i < pIndices.size() - 2; i++) {
+				if (i % 2 == 0) {
+					addToListIfAreaNonNull(pIndices.get(i), pIndices.get(i + 1), pIndices.get(i + 2), indices);
+				} else {
+					addToListIfAreaNonNull(pIndices.get(i), pIndices.get(i + 2), pIndices.get(i + 1), indices);
+				}
+			}
+			break;
+		case GL.GL_TRIANGLE_FAN:
+			Integer first = pIndices.get(0);
+			for (int i = 0; i < pIndices.size() - 2; i++) {
+				addToListIfAreaNonNull(first, pIndices.get(i + 1), pIndices.get(i + 2), indices);
+			}
+			break;
+		default:
+			throw new IllegalStateException("unknown type found: " + type);
+		}
+	}
+
+	public void addIndex(Integer i) {
+		pIndices.add(i);
+	}
+
+	private void addToListIfAreaNonNull(Integer i1, Integer i2, Integer i3, List<Integer> indices) {
+		Vector3d v1 = vertices.get(i1);
+		Vector3d v2 = vertices.get(i2);
+		Vector3d v3 = vertices.get(i3);
+		double area = Math.abs(calculateAreaOfTriangle(v1, v2, v3));
+		if (area > AREA_EPSILON) {
+			indices.add(i1);
+			indices.add(i2);
+			indices.add(i3);
+		}
+	}
+	
+	/**
+	 * Calculates the area of a triangle given by the 3 points
+	 * 
+	 * @param v1 the first point of the triangle
+	 * @param v2 the second point of the triangle
+	 * @param v3 the last point of the triangle
+	 * @return the area of the triangle
+	 */
+	private static double calculateAreaOfTriangle(Vector3d v1, Vector3d v2, Vector3d v3) {
+		double a = v1.getDistance(v2);
+		double b = v2.getDistance(v3);
+		double c = v1.getDistance(v3);
+		double s = 0.5d * (a + b + c);
+		return Math.sqrt(s * (s - a) * (s - b) * (s - c));
+	}
+
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/Ring.java b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/Ring.java
new file mode 100644
index 0000000000000000000000000000000000000000..823a27eabb967ac1af72022aba6f12219b20235e
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/Ring.java
@@ -0,0 +1,68 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.datastructure;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.hft.stuttgart.citygml.viewer.math.Vector3d;
+
+public class Ring {
+
+	private List<Vector3d> vertices = new ArrayList<>();
+
+	public List<Vector3d> getVertices() {
+		return vertices;
+	}
+
+	public void addPoint(Vector3d v) {
+		vertices.add(v);
+	}
+
+	public Vector3d calculateNormal() {
+		double[] coords = new double[3];
+		for (int i = 0; i < vertices.size() - 1; i++) {
+			Vector3d current = vertices.get(i + 0);
+			Vector3d next = vertices.get(i + 1);
+			coords[0] += (current.getZ() + next.getZ()) * (current.getY() - next.getY());
+			coords[1] += (current.getX() + next.getX()) * (current.getZ() - next.getZ());
+			coords[2] += (current.getY() + next.getY()) * (current.getX() - next.getX());
+		}
+
+		if (coords[0] == 0 && coords[1] == 0 && coords[2] == 0) {
+			// no valid normal vector found
+			if (vertices.size() < 3) {
+				// no three points, return x-axis
+				return new Vector3d(1, 0, 0);
+			}
+
+			Vector3d v1 = vertices.get(0);
+			Vector3d v2 = vertices.get(1);
+			Vector3d v3 = vertices.get(2);
+			return calculateNormalWithCross(v1, v2, v3);
+		}
+		Vector3d v = new Vector3d(coords);
+		v.normalize();
+		return v;
+	}
+
+	private Vector3d calculateNormalWithCross(Vector3d v1, Vector3d v2, Vector3d v3) {
+		Vector3d dir1 = v2.minus(v1);
+		Vector3d dir2 = v3.minus(v1);
+		Vector3d cross = dir1.cross(dir2);
+		return cross.normalize();
+	}
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/TesselatedPolygon.java b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/TesselatedPolygon.java
new file mode 100644
index 0000000000000000000000000000000000000000..7fce866e4b2e0a10bfa627b897284fe7c8516919
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/TesselatedPolygon.java
@@ -0,0 +1,67 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.datastructure;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+
+import de.hft.stuttgart.citygml.viewer.math.Vector3d;
+
+public class TesselatedPolygon {
+	
+
+	private List<Triangle3d> triangles;
+	private Vector3d normal;
+	private Color color;
+	
+	public static TesselatedPolygon of(Polygon p) {
+		TesselatedPolygon tessP = JoglTesselator.tesselatePolygon(p);
+		tessP.normal = p.calculateNormal();
+		tessP.color = p.getColor();
+		return tessP;
+	}
+
+	public TesselatedPolygon(List<Triangle3d> triangles) {
+		this.triangles = triangles;
+	}
+
+	public TesselatedPolygon(List<Vector3d> vertices, List<Integer> indices) {
+		if (indices.size() % 3 != 0) {
+			throw new IllegalArgumentException("Number of indices is not a multiple of 3");
+		}
+		triangles = new ArrayList<>();
+		for (int i = 0; i < indices.size(); i = i + 3) {
+			Vector3d v1 = vertices.get(indices.get(i + 0));
+			Vector3d v2 = vertices.get(indices.get(i + 1));
+			Vector3d v3 = vertices.get(indices.get(i + 2));
+			triangles.add(new Triangle3d(v1, v2, v3));
+		}
+	}
+
+	public List<Triangle3d> getTriangles() {
+		return triangles;
+	}
+	
+	public Vector3d getNormal() {
+		return normal;
+	}
+
+	public Color getColor() {
+		return color;
+	}
+
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/TesselationException.java b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/TesselationException.java
new file mode 100644
index 0000000000000000000000000000000000000000..f0cf2ef48afc09a478ba8bdd1a72966a6c6e20f7
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/TesselationException.java
@@ -0,0 +1,49 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.datastructure;
+
+/**
+ * Thrown when something went wrong with the tesselation process.
+ * 
+ * @author Matthias Betz
+ *
+ */
+public class TesselationException extends RuntimeException {
+
+	public TesselationException() {
+		super();
+	}
+
+	public TesselationException(String message, Throwable cause, boolean enableSuppression,
+			boolean writableStackTrace) {
+		super(message, cause, enableSuppression, writableStackTrace);
+	}
+
+	public TesselationException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	public TesselationException(Throwable cause) {
+		super(cause);
+	}
+
+	private static final long serialVersionUID = -2010522579830781136L;
+
+	public TesselationException(String message) {
+		super(message);
+	}
+
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/Triangle3d.java b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/Triangle3d.java
new file mode 100644
index 0000000000000000000000000000000000000000..3fc6d162eff64373947de994f6bf82f4884b9644
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/datastructure/Triangle3d.java
@@ -0,0 +1,100 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.datastructure;
+
+import de.hft.stuttgart.citygml.viewer.math.Vector3d;
+
+/**
+ * A three dimensional triangle described by three points
+ * 
+ * @author Matthias Betz
+ *
+ */
+public class Triangle3d {
+
+	private Vector3d p1;
+	private Vector3d p2;
+	private Vector3d p3;
+
+	public Triangle3d(Vector3d p1, Vector3d p2, Vector3d p3) {
+		this.p1 = p1;
+		this.p2 = p2;
+		this.p3 = p3;
+	}
+
+	public Vector3d getP1() {
+		return p1;
+	}
+
+	public Vector3d getP2() {
+		return p2;
+	}
+
+	public Vector3d getP3() {
+		return p3;
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ((p1 == null) ? 0 : p1.hashCode());
+		result = prime * result + ((p2 == null) ? 0 : p2.hashCode());
+		result = prime * result + ((p3 == null) ? 0 : p3.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null) {
+			return false;
+		}
+		if (getClass() != obj.getClass()) {
+			return false;
+		}
+		Triangle3d other = (Triangle3d) obj;
+		if (p1 == null) {
+			if (other.p1 != null) {
+				return false;
+			}
+		} else if (!p1.equals(other.p1)) {
+			return false;
+		}
+		if (p2 == null) {
+			if (other.p2 != null) {
+				return false;
+			}
+		} else if (!p2.equals(other.p2)) {
+			return false;
+		}
+		if (p3 == null) {
+			if (other.p3 != null) {
+				return false;
+			}
+		} else if (!p3.equals(other.p3)) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public String toString() {
+		return "Triangle3d [p1=" + p1 + ", p2=" + p2 + ", p3=" + p3 + "]";
+	}
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/math/BoundingBoxCalculator.java b/src/main/java/de/hft/stuttgart/citygml/viewer/math/BoundingBoxCalculator.java
new file mode 100644
index 0000000000000000000000000000000000000000..46973e521dfc65d58d8fe92c47a4812a8bfd54b8
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/math/BoundingBoxCalculator.java
@@ -0,0 +1,162 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.math;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.hft.stuttgart.citygml.viewer.datastructure.BoundingBox;
+import de.hft.stuttgart.citygml.viewer.datastructure.Polygon;
+import de.hft.stuttgart.citygml.viewer.parser.FeatureMapper;
+
+/**
+ * Utility class for calculating axis aligned bounding boxes for different
+ * inputs.
+ * 
+ * @author Matthias Betz
+ *
+ */
+public class BoundingBoxCalculator {
+
+	private BoundingBoxCalculator() {
+		// only static use
+	}
+
+	/**
+	 * This will calculate two points where every other point in the geometry is
+	 * between those two. If the geometry does not contain points or is null, an
+	 * empty array is returned. This is considered an axis aligned bounding box.
+	 * Only the exterior rings of the polygons is used as inner rings should be
+	 * within the exterior or they are faulty.
+	 * 
+	 * @param a list of polygons containing the vertices
+	 * @return an BoundingBox containing the two end points of the bounding box.
+	 */
+	public static BoundingBox calculateBoundingBox(List<Polygon> polygons) {
+		double lowX = Double.MAX_VALUE;
+		double highX = Double.NEGATIVE_INFINITY;
+		double lowY = Double.MAX_VALUE;
+		double highY = Double.NEGATIVE_INFINITY;
+		double lowZ = Double.MAX_VALUE;
+		double highZ = Double.NEGATIVE_INFINITY;
+		for (Polygon p : polygons) {
+			// only need to check exterior rings
+			for (Vector3d v : p.getExteriorRing().getVertices()) {
+				if (v.getX() < lowX) {
+					lowX = v.getX();
+				}
+				if (v.getX() > highX) {
+					highX = v.getX();
+				}
+				if (v.getY() < lowY) {
+					lowY = v.getY();
+				}
+				if (v.getY() > highY) {
+					highY = v.getY();
+				}
+				if (v.getZ() < lowZ) {
+					lowZ = v.getZ();
+				}
+				if (v.getZ() > highZ) {
+					highZ = v.getZ();
+				}
+			}
+		}
+		Vector3d[] result = new Vector3d[2];
+		result[0] = new Vector3d(lowX, lowY, lowZ);
+		result[1] = new Vector3d(highX, highY, highZ);
+		return BoundingBox.of(result);
+	}
+
+	public static BoundingBox calculateBoundingBoxFromPoints(List<? extends Vector3d> points) {
+		double lowX = Double.MAX_VALUE;
+		double highX = Double.NEGATIVE_INFINITY;
+		double lowY = Double.MAX_VALUE;
+		double highY = Double.NEGATIVE_INFINITY;
+		double lowZ = Double.MAX_VALUE;
+		double highZ = Double.NEGATIVE_INFINITY;
+		// only need to check exterior rings
+		for (Vector3d v : points) {
+			if (v.getX() < lowX) {
+				lowX = v.getX();
+			}
+			if (v.getX() > highX) {
+				highX = v.getX();
+			}
+			if (v.getY() < lowY) {
+				lowY = v.getY();
+			}
+			if (v.getY() > highY) {
+				highY = v.getY();
+			}
+			if (v.getZ() < lowZ) {
+				lowZ = v.getZ();
+			}
+			if (v.getZ() > highZ) {
+				highZ = v.getZ();
+			}
+		}
+		Vector3d[] result = new Vector3d[2];
+		result[0] = new Vector3d(lowX, lowY, lowZ);
+		result[1] = new Vector3d(highX, highY, highZ);
+		return BoundingBox.of(result);
+	}
+
+	public static BoundingBox calculateBoundingBox(FeatureMapper mapper) {
+		List<List<Polygon>> polygonList = new ArrayList<>();
+		polygonList.add(mapper.getLod1Polygons());
+		polygonList.add(mapper.getLod2Polygons());
+		polygonList.add(mapper.getLod3Polygons());
+		polygonList.add(mapper.getLod4Polygons());
+
+		double lowX = Double.MAX_VALUE;
+		double highX = Double.NEGATIVE_INFINITY;
+		double lowY = Double.MAX_VALUE;
+		double highY = Double.NEGATIVE_INFINITY;
+		double lowZ = Double.MAX_VALUE;
+		double highZ = Double.NEGATIVE_INFINITY;
+		for (List<Polygon> polygons : polygonList) {
+
+			for (Polygon p : polygons) {
+				// only need to check exterior rings
+				for (Vector3d v : p.getExteriorRing().getVertices()) {
+					if (v.getX() < lowX) {
+						lowX = v.getX();
+					}
+					if (v.getX() > highX) {
+						highX = v.getX();
+					}
+					if (v.getY() < lowY) {
+						lowY = v.getY();
+					}
+					if (v.getY() > highY) {
+						highY = v.getY();
+					}
+					if (v.getZ() < lowZ) {
+						lowZ = v.getZ();
+					}
+					if (v.getZ() > highZ) {
+						highZ = v.getZ();
+					}
+				}
+			}
+		}
+		Vector3d[] result = new Vector3d[2];
+		result[0] = new Vector3d(lowX, lowY, lowZ);
+		result[1] = new Vector3d(highX, highY, highZ);
+		return BoundingBox.of(result);
+	}
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/math/Vector3d.java b/src/main/java/de/hft/stuttgart/citygml/viewer/math/Vector3d.java
new file mode 100644
index 0000000000000000000000000000000000000000..08466788fc44012e12c56f57e6f01f7c153791de
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/math/Vector3d.java
@@ -0,0 +1,299 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.math;
+
+import java.io.Serializable;
+import java.util.Arrays;
+
+public class Vector3d implements Serializable {
+
+	private static final long serialVersionUID = 3495650092142761365L;
+
+	private static final Vector3d ORIGIN = new Vector3d();
+
+	public static final int X = 0;
+	public static final int Y = 1;
+	public static final int Z = 2;
+
+	private double[] coords;
+
+	public Vector3d() {
+		this(0d, 0d, 0d);
+	}
+
+	public Vector3d(double x, double y, double z) {
+		coords = new double[3];
+		coords[0] = x;
+		coords[1] = y;
+		coords[2] = z;
+	}
+
+	public Vector3d(double[] coords) {
+		if (coords == null) {
+			throw new IllegalArgumentException("Coordinates can not be null");
+		}
+		this.coords = coords;
+	}
+
+	public Vector3d(Vector3d vec) {
+		coords = new double[3];
+		coords[0] = vec.getX();
+		coords[1] = vec.getY();
+		coords[2] = vec.getZ();
+	}
+
+	public double getX() {
+		return coords[0];
+	}
+
+	public double getY() {
+		return coords[1];
+	}
+
+	public double getZ() {
+		return coords[2];
+	}
+
+	public void setX(double x) {
+		coords[0] = x;
+	}
+
+	public void setY(double y) {
+		coords[1] = y;
+	}
+
+	public void setZ(double z) {
+		coords[2] = z;
+	}
+
+	/**
+	 * Getter for all coordinates. The changes made to this array are reflected in
+	 * this vector.
+	 * 
+	 * @return the coordiantes array
+	 */
+	public double[] getCoordinates() {
+		return coords;
+	}
+
+	public void setCoordinates(double[] coordiantes) {
+		if (coordiantes == null || coordiantes.length != 3) {
+			throw new IllegalArgumentException("Vector must have exactly 3 coordinates");
+		}
+		this.coords = coordiantes;
+	}
+
+	public double getLength() {
+		return getDistance(ORIGIN);
+	}
+
+	public double getSquaredLength() {
+		double x = coords[0];
+		double y = coords[1];
+		double z = coords[2];
+		return x * x + y * y + z * z;
+	}
+
+	public double getDistance(Vector3d other) {
+		return Math.sqrt(getDistanceSquare(other));
+	}
+
+	public double getDistanceSquare(Vector3d other) {
+		double x = coords[0] - other.getX();
+		double y = coords[1] - other.getY();
+		double z = coords[2] - other.getZ();
+		return x * x + y * y + z * z;
+	}
+
+	/**
+	 * adds another vector to this one. The result is
+	 * <code><br>(a1 + b1)<br>(a2 + b2)<br>(a3 + b3)</code>
+	 * 
+	 * @param b the other vector
+	 * @return the sum of both vectors as a new vector. Values in this instance
+	 *         remain unchanged.
+	 */
+	public Vector3d plus(Vector3d b) {
+		double x = coords[0] + b.getX();
+		double y = coords[1] + b.getY();
+		double z = coords[2] + b.getZ();
+		return new Vector3d(x, y, z);
+	}
+
+	/**
+	 * add a value to a coordinate. This will change the coordinates of this vector.
+	 * 
+	 * @param coordinateIndex one of <code>Vector3d.X, Vector3d.Y, Vector3d.Z</code>
+	 * @param value           the added value
+	 * @return the current instance for chaining commands
+	 */
+	public Vector3d plus(int coordinateIndex, double value) {
+		if (coordinateIndex < 0 || coordinateIndex > 2) {
+			throw new IllegalArgumentException("coordinateIndex has to be between 0 and 2");
+		}
+		coords[coordinateIndex] += value;
+		return this;
+	}
+
+	/**
+	 * subtract a value to a coordinate. This will change the coordinates of this
+	 * vector.
+	 * 
+	 * @param coordinateIndex one of <code>Vector3d.X, Vector3d.Y, Vector3d.Z</code>
+	 * @param value           the subtracted value
+	 * @return the current instance for chaining commands
+	 */
+	public Vector3d minus(int coordinateIndex, double value) {
+		if (coordinateIndex < 0 || coordinateIndex > 2) {
+			throw new IllegalArgumentException("coordinateIndex has to be between 0 and 2");
+		}
+		coords[coordinateIndex] -= value;
+		return this;
+	}
+
+	/**
+	 * subtract another vector from this one. The result is
+	 * <code><br>(a1 - b1)<br>(a2 - b2)<br>(a3 - b3)</code>
+	 * 
+	 * @param b the other vector
+	 * @return the result as a new vector. Values in this instance remain unchanged.
+	 */
+	public Vector3d minus(Vector3d b) {
+		double x = coords[0] - b.getX();
+		double y = coords[1] - b.getY();
+		double z = coords[2] - b.getZ();
+		return new Vector3d(x, y, z);
+	}
+
+	/**
+	 * multiply this vector with a scalar.
+	 * 
+	 * @param scalar the scalar
+	 * @return the result as a new vector. Values in this instance remain unchanged.
+	 */
+	public Vector3d mult(double scalar) {
+		double x = coords[0] * scalar;
+		double y = coords[1] * scalar;
+		double z = coords[2] * scalar;
+		return new Vector3d(x, y, z);
+	}
+
+	/**
+	 * normalizes this vector. This method changes the coordinates of this instance.
+	 */
+	public Vector3d normalize() {
+		double length = getLength();
+		// if the length is already 1, do nothing
+		final double epsilon = 0.0000001;
+		if (Math.abs(1 - length) < epsilon) {
+			return this;
+		}
+		coords[0] /= length;
+		coords[1] /= length;
+		coords[2] /= length;
+		return this;
+	}
+
+	/**
+	 * calculates the cross product. The result is
+	 * <code><br>(a2 * b3 - a3 * b2)<br>(a3 * b1 - a1 * b3)<br>(a1 * b2 - a2 * b1)</code>
+	 * 
+	 * @param b the other vector
+	 * @return the result as a new vector. Values in this instance remain unchanged.
+	 */
+	public Vector3d cross(Vector3d b) {
+		double x = coords[1] * b.getZ() - coords[2] * b.getY();
+		double y = coords[2] * b.getX() - coords[0] * b.getZ();
+		double z = coords[0] * b.getY() - coords[1] * b.getX();
+		return new Vector3d(x, y, z);
+	}
+
+	/**
+	 * Calculates the dot product. The result is
+	 * <code><br>(a1 * b1) + (a2 * b2) + (a3 * b3)<br></code>
+	 * 
+	 * @param b the other vector
+	 * @return the dot product
+	 */
+	public double dot(Vector3d b) {
+		return coords[0] * b.getX() + coords[1] * b.getY() + coords[2] * b.getZ();
+	}
+
+	/**
+	 * creates a new copy of this vector.
+	 * 
+	 * @return a copy of this vector
+	 */
+	public Vector3d copy() {
+		return new Vector3d(coords[0], coords[1], coords[2]);
+	}
+
+	@Override
+	public String toString() {
+		final int maxLen = 5;
+		StringBuilder builder = new StringBuilder();
+		builder.append("Vector3d [coords=");
+		builder.append(coords != null ? Arrays.toString(Arrays.copyOf(coords, Math.min(coords.length, maxLen))) : null);
+		builder.append("]");
+		return builder.toString();
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + Arrays.hashCode(coords);
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		Vector3d other = (Vector3d) obj;
+		return Arrays.equals(coords, other.coords);
+	}
+
+	/**
+	 * Two vectors are equals, when the distance between both is less than epsilon
+	 * 
+	 * @param other   the other vector
+	 * @param epsilon the distance where the vectors are considered the same
+	 * @return true if they are within epsilon distance, false otherwise.
+	 */
+	public boolean equalsWithEpsilon(Vector3d other, double epsilon) {
+		if (other == this) {
+			return true;
+		}
+		Vector3d dif = this.minus(other);
+		double sqLength = dif.getSquaredLength();
+		return sqLength < (epsilon * epsilon);
+	}
+
+	public double getCoordinate(int axis) {
+		return coords[axis];
+	}
+
+	public void plus(double radius) {
+		coords[0] += radius;
+		coords[1] += radius;
+		coords[2] += radius;
+	}
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/parser/CityGMLParser.java b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/CityGMLParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..8fdfd5dc7a56b4e2e380c17a2d195efedd870ba6
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/CityGMLParser.java
@@ -0,0 +1,205 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.parser;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.locationtech.proj4j.BasicCoordinateTransform;
+import org.locationtech.proj4j.CRSFactory;
+import org.locationtech.proj4j.CoordinateReferenceSystem;
+import org.locationtech.proj4j.ProjCoordinate;
+import org.locationtech.proj4j.units.Units;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
+
+import de.hft.stuttgart.citygml.viewer.CityGmlHandler;
+import de.hft.stuttgart.citygml.viewer.math.Vector3d;
+
+public class CityGMLParser {
+	
+	private static final Logger log = Logger.getLogger(CityGMLParser.class.getName());
+	
+	private static final SAXParserFactory FACTORY;
+	private static final CRSFactory CRS_FACTORY = new CRSFactory();
+	
+	// EPSG:31467
+	private static final Pattern P_EPSG = Pattern.compile("^(EPSG:\\d+)$");
+	// urn:ogc:def:crs,crs:EPSG:6.12:31467,crs:EPSG:6.12:5783
+	// or
+	// urn:ogc:def:crs,crs:EPSG::28992
+	private static final Pattern P_OGC = Pattern.compile("urn:ogc:def:crs,crs:EPSG:[\\d\\.]*:([\\d]+)\\D*");
+
+	private static final Pattern P_OGC2 = Pattern.compile("urn:ogc:def:crs:EPSG:[\\d\\.]*:([\\d]+)\\D*");
+
+	// urn:adv:crs:DE_DHDN_3GK3*DE_DHHN92_NH
+	// urn:adv:crs:ETRS89_UTM32*DE_DHHN92_NH
+	private static final Pattern P_URN = Pattern.compile("urn:adv:crs:([^\\*]+)");
+	private static final String WGS_84 = "EPSG:4326";
+
+
+
+	static {
+		FACTORY = SAXParserFactory.newInstance();
+		try {
+			FACTORY.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+		} catch (SAXNotRecognizedException | SAXNotSupportedException | ParserConfigurationException e) {
+			e.printStackTrace();
+		}
+	}
+	
+	public static void parseEpsgCodeFromFile(File file, ParserConfiguration config)
+			throws IOException, ParserConfigurationException, SAXException {
+		try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
+			parseEpsgCodeFromStream(bis, config);
+		}
+	}
+	
+	private static void parseEpsgCodeFromStream(InputStream is, ParserConfiguration config)
+			throws ParserConfigurationException, SAXException {
+		SAXParser parser = FACTORY.newSAXParser();
+		CityGmlHandler handler = new CityGmlHandler();
+		try {
+			parser.parse(new InputSource(is), handler);
+		} catch (EnvelopeFoundException e) {
+			try {
+				parseCoordinateSystem(config, handler);
+			} catch (Exception e2) {
+				log.log(Level.WARNING, "Caught unknown error while parsing EPSG, assuming metric coordinate system", e2);
+			}
+		} catch (Exception e) {
+			log.log(Level.WARNING, "Caught unknown error while parsing EPSG, assuming metric coordinate system", e);
+		}
+	}
+	
+	private static void parseCoordinateSystem(ParserConfiguration config, CityGmlHandler handler) {
+		if (handler.getEpsg() == null) {
+			return;
+		}
+		CoordinateReferenceSystem crs = crsFromSrsName(handler.getEpsg());
+		if (crs == null) {
+			// could not find a coordinate system for srsName
+			// assuming metric system
+			return;
+		}
+		if (crs.getProjection().getUnits() == Units.METRES) {
+			// coordinate system is in meters, do not convert
+			return;
+		}
+		Vector3d low = handler.getLowerCorner();
+		Vector3d up = handler.getUpperCorner();
+		double centerLong = low.getX() + ((up.getX() - low.getX()) / 2);
+		double centerLat = low.getY() + ((up.getY() - low.getY()) / 2);
+		if (!crs.getName().equals(WGS_84)) {
+			// need to convert coordinates first to WGS84, then find UTM Zone
+			CoordinateReferenceSystem wgs84 = crsFromSrsName(WGS_84);
+			ProjCoordinate p1 = new ProjCoordinate();
+			p1.setValue(centerLong, centerLat);
+			ProjCoordinate p2 = new ProjCoordinate();
+			BasicCoordinateTransform bct = new BasicCoordinateTransform(crs, wgs84);
+			bct.transform(p1, p2);
+			centerLong = p2.x;
+			centerLat = p2.y;
+		}
+		int zone = (int) (31 + Math.round(centerLong / 6));
+		CoordinateReferenceSystem utm;
+		if (centerLat < 0) {
+			// south
+			utm = CRS_FACTORY.createFromParameters("UTM", "+proj=utm +ellps=WGS84 +units=m +zone=" + zone + " +south");
+		} else {
+			// north
+			utm = CRS_FACTORY.createFromParameters("UTM", "+proj=utm +ellps=WGS84 +units=m +zone=" + zone);
+		}
+		config.setCoordinateSystem(crs, utm);
+	}
+	
+	/**
+	 * The srsName (The name by which this reference system is identified) inside
+	 * the CityGML file can have multiple formats. This method tries to parse the
+	 * string and detect the corresponding reference system. If it is found, it
+	 * returns a proj4j.CoordinateReferenceSystem. It throws an
+	 * IllegalArgumentException otherwise.
+	 * 
+	 * This method should be able to parse any EPSG id : e.g. "EPSG:1234". German
+	 * Citygmls might also have "DE_DHDN_3GK3" or "ETRS89_UTM32" as srsName, so
+	 * those are also included. It isn't guaranteed that those formats are correctly
+	 * parsed, though.
+	 * 
+	 * The EPSG ids and parameters are defined in resources ('nad/epsg') inside
+	 * proj4j-0.1.0.jar. Some EPSG ids are missing though, e.g. 7415
+	 * 
+	 * @param srsName
+	 * @return CoordinateReferenceSystem
+	 */
+	private static CoordinateReferenceSystem crsFromSrsName(String srsName) {
+		srsName = srsName.trim();
+		Matcher mEPSG = P_EPSG.matcher(srsName);
+		if (mEPSG.find()) {
+			if ("EPSG:4979".contentEquals(srsName)) {
+				srsName = "EPSG:4236";
+			} else if ("EPSG:7415".contentEquals(srsName)) {
+				return CRS_FACTORY.createFromParameters("EPSG:7415",
+						"+proj=sterea +lat_0=52.15616055555555 +lon_0=5.38763888888889 +k=0.9999079 +x_0=155000 +y_0=463000 +ellps=bessel +towgs84=565.417,50.3319,465.552,-0.398957,0.343988,-1.8774,4.0725 +units=m +no_defs");
+			}
+			return CRS_FACTORY.createFromName(srsName);
+		}
+
+		Matcher mOGC = P_OGC.matcher(srsName);
+		if (mOGC.find()) {
+			return CRS_FACTORY.createFromName("EPSG:" + mOGC.group(1));
+		}
+		Matcher mOGC2 = P_OGC2.matcher(srsName);
+		if (mOGC2.find()) {
+			return CRS_FACTORY.createFromName("EPSG:" + mOGC2.group(1));
+		}
+		Matcher mURN = P_URN.matcher(srsName);
+		// NOTE: Could use a HashMap if the switch/case becomes too long.
+		if (mURN.find()) {
+			switch (mURN.group(1)) {
+			case "DE_DHDN_3GK2":
+				return CRS_FACTORY.createFromName("EPSG:31466");
+			case "DE_DHDN_3GK3":
+				return CRS_FACTORY.createFromName("EPSG:31467");
+			case "DE_DHDN_3GK4":
+				return CRS_FACTORY.createFromName("EPSG:31468");
+			case "DE_DHDN_3GK5":
+				return CRS_FACTORY.createFromName("EPSG:31469");
+			case "ETRS89_UTM32":
+				return CRS_FACTORY.createFromName("EPSG:25832");
+			default:
+				return null;
+			}
+		}
+		if (srsName.equals("http://www.opengis.net/def/crs/EPSG/0/6697")) {
+			return CRS_FACTORY.createFromParameters("EPSG:6697", "+proj=longlat +ellps=GRS80 +no_defs +axis=neu");
+		}
+		return null;
+	}
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/parser/EnvelopeFoundException.java b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/EnvelopeFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..8dfe1116452bb2ab7e900798a132c5c307b53cde
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/EnvelopeFoundException.java
@@ -0,0 +1,47 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.parser;
+
+import org.xml.sax.SAXException;
+
+/**
+ * To stop the SAXParser from further parsing when the relevant section has
+ * already been found.
+ * 
+ * @author Matthias Betz
+ *
+ */
+public class EnvelopeFoundException extends SAXException {
+
+	private static final long serialVersionUID = -9188617211115043815L;
+
+	public EnvelopeFoundException() {
+		super();
+	}
+
+	public EnvelopeFoundException(Exception e) {
+		super(e);
+	}
+
+	public EnvelopeFoundException(String message, Exception e) {
+		super(message, e);
+	}
+
+	public EnvelopeFoundException(String message) {
+		super(message);
+	}
+
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/parser/FeatureMapper.java b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/FeatureMapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..680894e521d3de41f27e5963f7379a804650a4cf
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/FeatureMapper.java
@@ -0,0 +1,498 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.parser;
+
+import java.awt.Color;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+import org.citygml4j.model.bridge.AbstractBridge;
+import org.citygml4j.model.building.AbstractBuilding;
+import org.citygml4j.model.building.BuildingInstallation;
+import org.citygml4j.model.construction.AbstractFillingElement;
+import org.citygml4j.model.construction.Door;
+import org.citygml4j.model.construction.GroundSurface;
+import org.citygml4j.model.construction.RoofSurface;
+import org.citygml4j.model.construction.Window;
+import org.citygml4j.model.core.AbstractThematicSurface;
+import org.citygml4j.model.core.ImplicitGeometry;
+import org.citygml4j.model.core.ImplicitGeometryProperty;
+import org.citygml4j.model.deprecated.bridge.DeprecatedPropertiesOfAbstractBridge;
+import org.citygml4j.model.deprecated.building.DeprecatedPropertiesOfAbstractBuilding;
+import org.citygml4j.model.deprecated.building.DeprecatedPropertiesOfBuildingInstallation;
+import org.citygml4j.model.deprecated.core.DeprecatedPropertiesOfAbstractThematicSurface;
+import org.citygml4j.model.deprecated.transportation.DeprecatedPropertiesOfAbstractTransportationSpace;
+import org.citygml4j.model.deprecated.transportation.TransportationComplex;
+import org.citygml4j.model.deprecated.vegetation.DeprecatedPropertiesOfPlantCover;
+import org.citygml4j.model.deprecated.vegetation.DeprecatedPropertiesOfSolitaryVegetationObject;
+import org.citygml4j.model.deprecated.waterbody.DeprecatedPropertiesOfWaterBody;
+import org.citygml4j.model.landuse.LandUse;
+import org.citygml4j.model.relief.TINRelief;
+import org.citygml4j.model.transportation.AuxiliaryTrafficArea;
+import org.citygml4j.model.transportation.TrafficArea;
+import org.citygml4j.model.vegetation.PlantCover;
+import org.citygml4j.model.vegetation.SolitaryVegetationObject;
+import org.citygml4j.model.waterbody.WaterBody;
+import org.citygml4j.visitor.ObjectWalker;
+import org.locationtech.proj4j.BasicCoordinateTransform;
+import org.locationtech.proj4j.ProjCoordinate;
+import org.xmlobjects.gml.model.geometry.AbstractGeometry;
+import org.xmlobjects.gml.model.geometry.GeometricPositionList;
+import org.xmlobjects.gml.model.geometry.GeometryProperty;
+import org.xmlobjects.gml.model.geometry.primitives.AbstractRing;
+import org.xmlobjects.gml.model.geometry.primitives.AbstractRingProperty;
+import org.xmlobjects.gml.model.geometry.primitives.LinearRing;
+import org.xmlobjects.gml.model.geometry.primitives.Triangle;
+import org.xmlobjects.gml.model.geometry.primitives.TriangulatedSurface;
+
+import de.hft.stuttgart.citygml.viewer.datastructure.Polygon;
+import de.hft.stuttgart.citygml.viewer.datastructure.Ring;
+import de.hft.stuttgart.citygml.viewer.math.Vector3d;
+
+public class FeatureMapper extends ObjectWalker {
+
+	private Color groundColor = new Color(0.9411765f, 0.9019608f, 0.54901963f);
+	private Color roofColor = Color.RED;
+	private Color doorColor = Color.ORANGE;
+	private Color windowColor = new Color(0.0f, 0.5019608f, 0.5019608f);
+	private Color wallColor = Color.WHITE;
+	private Color bridgeColor = new Color(1.0f, 0.49803922f, 0.3137255f);
+	private Color landColor = new Color(0.64705884f, 0.16470589f, 0.16470589f);
+	private Color transportationColor = new Color(1.0f, 1.0f, 0.0f);
+	private Color vegetationColor = new Color(0.5647059f, 0.93333334f, 0.5647059f);
+	private Color waterColor = new Color(0.5294118f, 0.80784315f, 0.98039216f);
+
+	private ParserConfiguration config;
+	private List<Polygon> lod1Polygons;
+	private List<Polygon> lod2Polygons;
+	private List<Polygon> lod3Polygons;
+	private List<Polygon> lod4Polygons;
+
+	private List<Polygon> currentPolygons = null;
+	private Ring currentRing;
+	private Color currentColor;
+
+	private ProjCoordinate p1 = new ProjCoordinate();
+	private ProjCoordinate p2 = new ProjCoordinate();
+
+	public FeatureMapper(ParserConfiguration config) throws IOException {
+		try (FileReader reader = new FileReader("color.properties")) {
+			Properties props = new Properties();
+			props.load(reader);
+			Color parsedGroundColor = parseColor(props.getProperty("groundColor"));
+			if (parsedGroundColor != null) {
+				groundColor = parsedGroundColor;
+			}
+			Color parsedRoofColor = parseColor(props.getProperty("roofColor"));
+			if (parsedRoofColor != null) {
+				roofColor = parsedRoofColor;
+			}
+			Color parsedDoorColor = parseColor(props.getProperty("doorColor"));
+			if (parsedDoorColor != null) {
+				doorColor = parsedDoorColor;
+			}
+			Color parsedWindowColor = parseColor(props.getProperty("windowColor"));
+			if (parsedWindowColor != null) {
+				windowColor = parsedWindowColor;
+			}
+			Color parsedWallColor = parseColor(props.getProperty("wallColor"));
+			if (parsedWallColor != null) {
+				wallColor = parsedWallColor;
+			}
+			Color parsedBridgeColor = parseColor(props.getProperty("bridgeColor"));
+			if (parsedBridgeColor != null) {
+				bridgeColor = parsedBridgeColor;
+			}
+			Color parsedLandColor = parseColor(props.getProperty("landColor"));
+			if (parsedLandColor != null) {
+				landColor = parsedLandColor;
+			}
+			Color parsedTransportationColor = parseColor(props.getProperty("transportationColor"));
+			if (parsedTransportationColor != null) {
+				transportationColor = parsedTransportationColor;
+			}
+			Color parsedVegetationColor = parseColor(props.getProperty("vegetationColor"));
+			if (parsedVegetationColor != null) {
+				vegetationColor = parsedVegetationColor;
+			}
+			Color parsedWaterColor = parseColor(props.getProperty("waterColor"));
+			if (parsedWaterColor != null) {
+				waterColor = parsedWaterColor;
+			}
+		}
+		this.config = config;
+		lod1Polygons = new ArrayList<>();
+		lod2Polygons = new ArrayList<>();
+		lod3Polygons = new ArrayList<>();
+		lod4Polygons = new ArrayList<>();
+	}
+
+	private Color parseColor(String colorString) {
+		if (colorString == null) {
+			return null;
+		}
+		try {
+			String[] split = colorString.split(" ");
+			float r = Float.parseFloat(split[0]);
+			float g = Float.parseFloat(split[1]);
+			float b = Float.parseFloat(split[2]);
+			return new Color(r, g, b);
+		} catch (Exception e) {
+			e.printStackTrace();
+			return null;
+		}
+	}
+
+	@Override
+	public void visit(org.xmlobjects.gml.model.geometry.primitives.Polygon gmlPoly) {
+		if (currentPolygons == null) {
+			return;
+		}
+		Polygon viewerPoly = new Polygon(currentColor);
+		// parse rings
+		Ring extRing = new Ring();
+		viewerPoly.setExteriorRing(extRing);
+		mapRing(gmlPoly.getExterior(), extRing);
+
+		if (gmlPoly.getInterior() != null) {
+			for (AbstractRingProperty arp : gmlPoly.getInterior()) {
+				if (arp.getObject() != null) {
+					Ring innerRing = new Ring();
+					viewerPoly.addInteriorRing(innerRing);
+					mapRing(arp, innerRing);
+				}
+			}
+		}
+		currentPolygons.add(viewerPoly);
+	}
+	
+	@Override
+	public void visit(Triangle triangle) {
+		if (currentPolygons == null) {
+			return;
+		}
+		// triangle is only a special case of a polygon
+		// so use a polygon
+		Polygon viewerPoly = new Polygon(currentColor);
+		// parse rings
+		Ring extRing = new Ring();
+		viewerPoly.setExteriorRing(extRing);
+		mapRing(triangle.getExterior(), extRing);
+		currentPolygons.add(viewerPoly);
+	}
+	
+	private void mapRing(AbstractRingProperty gmlRing, Ring viewerRing) {
+		if (gmlRing == null || gmlRing.getObject() == null) {
+			return;
+		}
+		AbstractRing ringGeometry = gmlRing.getObject();
+		currentRing = viewerRing;
+		// jump to LinearRing or Ring visit
+		ringGeometry.accept(this);
+	}
+
+	@Override
+	public void visit(LinearRing linearRing) {
+		if (linearRing.getControlPoints() != null) {
+			int dimension = getDimension(linearRing);
+			parseControlPoints(linearRing.getControlPoints(), dimension);
+		}
+	}
+
+	private int getDimension(LinearRing linearRing) {
+		int dimension = 3;
+		if (linearRing.getSrsDimension() != null) {
+			dimension = linearRing.getSrsDimension();
+		}
+		return dimension;
+	}
+
+	private void parseControlPoints(GeometricPositionList controlPoints, int dimension) {
+		List<Double> coords = controlPoints.toCoordinateList3D();
+		switch (dimension) {
+		case 1:
+			for (int i = 0; i < coords.size(); i++) {
+				createVertex(coords.get(i), 0, 0);
+			}
+			break;
+		case 2:
+			for (int i = 0; i < coords.size(); i = i + 2) {
+				createVertex(coords.get(i + 0), coords.get(i + 1), 0);
+			}
+			break;
+		case 3:
+			for (int i = 0; i < coords.size(); i = i + 3) {
+				createVertex(coords.get(i + 0), coords.get(i + 1), coords.get(i + 2));
+			}
+			break;
+		default:
+			throw new UnsupportedOperationException("Cannot parse Coordinates with dimension:" + dimension);
+		}
+	}
+
+	private void createVertex(double x, double y, double z) {
+		// transform into utm, if available
+		BasicCoordinateTransform trans = config.getTargetTransform();
+		if (trans != null) {
+			p1.setValue(x, y);
+			trans.transform(p1, p2);
+			x = p2.x;
+			y = p2.y;
+		}
+		Vector3d v = new Vector3d(x, y, z);
+		currentRing.addPoint(v);
+	}
+
+	@Override
+	public void visit(AbstractBuilding ab) {
+		super.visit(ab);
+		DeprecatedPropertiesOfAbstractBuilding deprecatedProperties = ab.getDeprecatedProperties();
+		parseAbstractGeometry(deprecatedProperties.getLod1MultiSurface(), lod1Polygons, wallColor);
+		parseAbstractGeometry(ab.getLod2MultiSurface(), lod2Polygons, wallColor);
+		parseAbstractGeometry(ab.getLod3MultiSurface(), lod3Polygons, wallColor);
+		parseAbstractGeometry(deprecatedProperties.getLod4MultiSurface(), lod4Polygons, wallColor);
+		parseAbstractGeometry(ab.getLod1Solid(), lod1Polygons, wallColor);
+		parseAbstractGeometry(ab.getLod2Solid(), lod2Polygons, wallColor);
+		parseAbstractGeometry(ab.getLod3Solid(), lod3Polygons, wallColor);
+		parseAbstractGeometry(deprecatedProperties.getLod4Solid(), lod4Polygons, wallColor);
+	}
+	
+	@Override
+	public void visit(TINRelief tinRelief) {
+		super.visit(tinRelief);
+		if (tinRelief.getTin() == null || tinRelief.getTin().getObject() == null) {
+			return;
+		}
+		TriangulatedSurface object = tinRelief.getTin().getObject();
+		currentPolygons = lod1Polygons;
+		currentColor = landColor;
+		object.accept(this);
+		currentPolygons = null;
+		currentColor = null;
+	}
+
+	@Override
+	public void visit(WaterBody wb) {
+		super.visit(wb);
+		DeprecatedPropertiesOfWaterBody deprecatedProperties = wb.getDeprecatedProperties();
+		parseAbstractGeometry(deprecatedProperties.getLod1MultiSurface(), lod1Polygons, waterColor);
+		parseAbstractGeometry(wb.getLod1Solid(), lod1Polygons, waterColor);
+		parseAbstractGeometry(wb.getLod2Solid(), lod2Polygons, waterColor);
+		parseAbstractGeometry(wb.getLod3Solid(), lod3Polygons, waterColor);
+		parseAbstractGeometry(deprecatedProperties.getLod4Solid(), lod4Polygons, waterColor);
+	}
+
+	@Override
+	public void visit(PlantCover pc) {
+		super.visit(pc);
+		DeprecatedPropertiesOfPlantCover deprecatedProperties = pc.getDeprecatedProperties();
+		parseAbstractGeometry(deprecatedProperties.getLod1MultiSurface(), lod1Polygons, vegetationColor);
+		parseAbstractGeometry(pc.getLod2MultiSurface(), lod2Polygons, vegetationColor);
+		parseAbstractGeometry(pc.getLod3MultiSurface(), lod3Polygons, vegetationColor);
+		parseAbstractGeometry(deprecatedProperties.getLod4MultiSurface(), lod4Polygons, vegetationColor);
+		parseAbstractGeometry(deprecatedProperties.getLod1MultiSolid(), lod1Polygons, vegetationColor);
+		parseAbstractGeometry(deprecatedProperties.getLod2MultiSolid(), lod2Polygons, vegetationColor);
+		parseAbstractGeometry(deprecatedProperties.getLod3MultiSolid(), lod3Polygons, vegetationColor);
+		parseAbstractGeometry(deprecatedProperties.getLod4MultiSolid(), lod4Polygons, vegetationColor);
+	}
+
+	@Override
+	public void visit(SolitaryVegetationObject svo) {
+		super.visit(svo);
+		DeprecatedPropertiesOfSolitaryVegetationObject deprecatedProperties = svo.getDeprecatedProperties();
+		parseAbstractGeometry(deprecatedProperties.getLod1Geometry(), lod1Polygons, vegetationColor);
+		parseAbstractGeometry(deprecatedProperties.getLod2Geometry(), lod2Polygons, vegetationColor);
+		parseAbstractGeometry(deprecatedProperties.getLod3Geometry(), lod3Polygons, vegetationColor);
+		parseAbstractGeometry(deprecatedProperties.getLod4Geometry(), lod4Polygons, vegetationColor);
+		parseImplicitGeometry(svo.getLod1ImplicitRepresentation(), lod1Polygons, vegetationColor);
+		parseImplicitGeometry(svo.getLod2ImplicitRepresentation(), lod2Polygons, vegetationColor);
+		parseImplicitGeometry(svo.getLod3ImplicitRepresentation(), lod3Polygons, vegetationColor);
+		parseImplicitGeometry(deprecatedProperties.getLod4ImplicitRepresentation(), lod4Polygons, vegetationColor);
+	}
+
+	@Override
+	public void visit(AbstractThematicSurface abs) {
+		super.visit(abs);
+		Color color = getBuildingSurfaceColor(abs);
+		DeprecatedPropertiesOfAbstractThematicSurface deprecatedProperties = abs.getDeprecatedProperties();
+		parseAbstractGeometry(abs.getLod2MultiSurface(), lod2Polygons, color);
+		parseAbstractGeometry(abs.getLod3MultiSurface(), lod3Polygons, color);
+		parseAbstractGeometry(deprecatedProperties.getLod4MultiSurface(), lod4Polygons, color);
+	}
+
+	@Override
+	public void visit(LandUse landUse) {
+		super.visit(landUse);
+		DeprecatedPropertiesOfAbstractThematicSurface deprecatedProperties = landUse.getDeprecatedProperties();
+		parseAbstractGeometry(landUse.getLod1MultiSurface(), lod1Polygons, landColor);
+		parseAbstractGeometry(landUse.getLod2MultiSurface(), lod2Polygons, landColor);
+		parseAbstractGeometry(landUse.getLod3MultiSurface(), lod3Polygons, landColor);
+		parseAbstractGeometry(deprecatedProperties.getLod4MultiSurface(), lod4Polygons, landColor);
+	}
+
+	@Override
+	public void visit(AuxiliaryTrafficArea ata) {
+		super.visit(ata);
+		DeprecatedPropertiesOfAbstractThematicSurface deprecatedProperties = ata.getDeprecatedProperties();
+		parseAbstractGeometry(ata.getLod2MultiSurface(), lod2Polygons, transportationColor);
+		parseAbstractGeometry(ata.getLod3MultiSurface(), lod3Polygons, transportationColor);
+		parseAbstractGeometry(deprecatedProperties.getLod4MultiSurface(), lod4Polygons, transportationColor);
+	}
+
+	@Override
+	public void visit(TrafficArea ta) {
+		super.visit(ta);
+		DeprecatedPropertiesOfAbstractThematicSurface deprecatedProperties = ta.getDeprecatedProperties();
+		parseAbstractGeometry(ta.getLod2MultiSurface(), lod2Polygons, transportationColor);
+		parseAbstractGeometry(ta.getLod3MultiSurface(), lod3Polygons, transportationColor);
+		parseAbstractGeometry(deprecatedProperties.getLod4MultiSurface(), lod4Polygons, transportationColor);
+	}
+
+	@Override
+	public void visit(TransportationComplex tc) {
+		super.visit(tc);
+		DeprecatedPropertiesOfAbstractTransportationSpace deprecatedProperties = tc.getDeprecatedProperties();
+		parseAbstractGeometry(deprecatedProperties.getLod1MultiSurface(), lod1Polygons, transportationColor);
+		parseAbstractGeometry(tc.getLod2MultiSurface(), lod2Polygons, transportationColor);
+		parseAbstractGeometry(tc.getLod3MultiSurface(), lod3Polygons, transportationColor);
+		parseAbstractGeometry(deprecatedProperties.getLod4MultiSurface(), lod4Polygons, transportationColor);
+	}
+
+	private Color getBuildingSurfaceColor(AbstractThematicSurface abs) {
+		if (abs instanceof RoofSurface) {
+			return roofColor;
+		}
+		if (abs instanceof GroundSurface) {
+			return groundColor;
+		}
+		return wallColor;
+	}
+
+	@Override
+	public void visit(AbstractFillingElement ao) {
+		super.visit(ao);
+		Color color = getOpeningColor(ao);
+		parseAbstractGeometry(ao.getLod1Solid(), lod1Polygons, color);
+		parseAbstractGeometry(ao.getLod2Solid(), lod2Polygons, color);
+		parseAbstractGeometry(ao.getLod3Solid(), lod3Polygons, color);
+		parseAbstractGeometry(ao.getLod2MultiSurface(), lod2Polygons, color);
+		parseAbstractGeometry(ao.getLod3MultiSurface(), lod3Polygons, color);
+		parseImplicitGeometry(ao.getLod1ImplicitRepresentation(), lod1Polygons, color);
+		parseImplicitGeometry(ao.getLod2ImplicitRepresentation(), lod2Polygons, color);
+		parseImplicitGeometry(ao.getLod3ImplicitRepresentation(), lod3Polygons, color);
+	}
+
+	private Color getOpeningColor(AbstractFillingElement ao) {
+		if (ao instanceof Door) {
+			return doorColor;
+		}
+		if (ao instanceof Window) {
+			return windowColor;
+		}
+		return wallColor;
+	}
+
+	@Override
+	public void visit(AbstractBridge ab) {
+		super.visit(ab);
+		DeprecatedPropertiesOfAbstractBridge deprecatedProperties = ab.getDeprecatedProperties();
+		parseAbstractGeometry(deprecatedProperties.getLod1MultiSurface(), lod1Polygons, bridgeColor);
+		parseAbstractGeometry(ab.getLod2MultiSurface(), lod2Polygons, bridgeColor);
+		parseAbstractGeometry(ab.getLod3MultiSurface(), lod3Polygons, bridgeColor);
+		parseAbstractGeometry(deprecatedProperties.getLod4MultiSurface(), lod4Polygons, bridgeColor);
+		parseAbstractGeometry(ab.getLod1Solid(), lod1Polygons, bridgeColor);
+		parseAbstractGeometry(ab.getLod2Solid(), lod2Polygons, bridgeColor);
+		parseAbstractGeometry(ab.getLod3Solid(), lod3Polygons, bridgeColor);
+		parseAbstractGeometry(deprecatedProperties.getLod4Solid(), lod4Polygons, bridgeColor);
+	}
+	
+	@Override
+	public void visit(BuildingInstallation bi) {
+		super.visit(bi);
+		DeprecatedPropertiesOfBuildingInstallation deprecatedProperties = bi.getDeprecatedProperties();
+		parseAbstractGeometry(deprecatedProperties.getLod2Geometry(), lod2Polygons, wallColor);
+		parseAbstractGeometry(deprecatedProperties.getLod3Geometry(), lod3Polygons, wallColor);
+		parseAbstractGeometry(deprecatedProperties.getLod4Geometry(), lod4Polygons, wallColor);
+		parseImplicitGeometry(bi.getLod2ImplicitRepresentation(), lod2Polygons, wallColor);
+		parseImplicitGeometry(bi.getLod3ImplicitRepresentation(), lod3Polygons, wallColor);
+		parseImplicitGeometry(deprecatedProperties.getLod4ImplicitRepresentation(), lod4Polygons, wallColor);
+	}
+
+	private void parseImplicitGeometry(ImplicitGeometryProperty irp, List<Polygon> polygons, Color color) {
+		if (irp == null || irp.getObject() == null) {
+			return;
+		}
+		ImplicitGeometry implicitGeometry = irp.getObject();
+		parseAbstractGeometry(implicitGeometry.getRelativeGeometry(), polygons, color);
+	}
+
+	private void parseAbstractGeometry(GeometryProperty<? extends AbstractGeometry> geom, List<Polygon> polygons,
+			Color color) {
+		if (geom == null || geom.getObject() == null) {
+			return;
+		}
+		currentColor = color;
+		currentPolygons = polygons;
+		geom.getObject().accept(this);
+		currentColor = null;
+		currentPolygons = null;
+	}
+
+	public List<Polygon> getLod1Polygons() {
+		return lod1Polygons;
+	}
+
+	public List<Polygon> getLod2Polygons() {
+		return lod2Polygons;
+	}
+
+	public List<Polygon> getLod3Polygons() {
+		return lod3Polygons;
+	}
+
+	public List<Polygon> getLod4Polygons() {
+		return lod4Polygons;
+	}
+
+	public void movePolygonsBy(Vector3d center) {
+		movePolygonsBy(lod1Polygons, center);
+		movePolygonsBy(lod2Polygons, center);
+		movePolygonsBy(lod3Polygons, center);
+		movePolygonsBy(lod4Polygons, center);
+	}
+
+	private void movePolygonsBy(List<Polygon> polygons, Vector3d center) {
+		for (Polygon p : polygons) {
+			for (Vector3d v : p.getExteriorRing().getVertices()) {
+				movePointBy(v, center);
+			}
+			for (Ring r : p.getInteriorRings()) {
+				for (Vector3d v : r.getVertices()) {
+					movePointBy(v, center);
+				}
+			}
+		}
+	}
+
+	private void movePointBy(Vector3d v, Vector3d center) {
+		v.setX(v.getX() - center.getX());
+		v.setY(v.getY() - center.getY());
+		v.setZ(v.getZ() - center.getZ());
+	}
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/parser/InputStreamListener.java b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/InputStreamListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..866ea4f0d61c66c11f338fe6ce8dbb2591752bf4
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/InputStreamListener.java
@@ -0,0 +1,35 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.parser;
+
+import java.util.EventListener;
+
+/**
+ * Implementations of this listener will be informed about the (file) loading progress of ObservedInputStream
+ * instances. You have to register your implementation of this interface at the ObservedInputStream instance.
+ * 
+ * @author Marcel Bruse
+ */
+public interface InputStreamListener extends EventListener {
+	
+	/**
+	 * This method will be called by the observed input stream when a new chunk of bytes has been read. 
+	 * 
+	 * @param progress Should be a value between 0 and 1. 0 means no progress and 1 means loading finished. 
+	 */
+	public void updateProgress(float progress);
+	
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/parser/LOD.java b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/LOD.java
new file mode 100644
index 0000000000000000000000000000000000000000..9145dad15e22916ca8371b9f6f14b7d8cd25d60a
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/LOD.java
@@ -0,0 +1,22 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.parser;
+
+public enum LOD {
+	
+	LOD_1, LOD_2, LOD_3, LOD_4
+
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/parser/ObservedInputStream.java b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/ObservedInputStream.java
new file mode 100644
index 0000000000000000000000000000000000000000..5d4672ff931e8f776daf92c88ab1d8b9a33462c6
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/ObservedInputStream.java
@@ -0,0 +1,115 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.parser;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * This ObservedInputStream will update its registered listeners about the reading process of the underlying file.
+ * Every time when the underlying input stream is read the registered listeners will be informed and updated with the
+ * new progress.
+ * 
+ * @author Marcel Bruse
+ */
+public class ObservedInputStream extends FilterInputStream {
+
+	/** List of registered progress listeners. */
+	private ArrayList<InputStreamListener> listeners = new ArrayList<>();
+	
+	/** The number of bytes of the file to be read. */
+	private long fileLength;
+	
+	/** The number of bytes read so far. */
+	private long location;
+	
+	/**
+	 * Instantiates the ObservedInputStream with a given file handle.
+	 * 
+	 * @param file The file that should be loaded.
+	 * @throws FileNotFoundException If there is on file, you will get some of this.
+	 */
+	public ObservedInputStream(File file) throws FileNotFoundException {
+		super(new BufferedInputStream(new FileInputStream(file)));
+		fileLength = file.length();
+	}
+	
+	public ObservedInputStream(String fileName) throws FileNotFoundException {
+		this(new File(fileName));
+	}
+	
+	public ObservedInputStream(InputStream in, long fileLength) {
+		super(new BufferedInputStream(in));
+		this.fileLength = fileLength;
+	}
+
+	/**
+	 * Calls all the registered listeners if the file loading process makes any progress.
+	 * It calculates and passes the progress in percent. 
+	 */
+	private void updateProgress() {
+		for (InputStreamListener l : listeners) {
+			l.updateProgress((float) location / fileLength);
+		}
+	}
+	
+	/**
+	 * Calls super and counts the number of read bytes. 
+	 * 
+	 * @see FilterInputStream
+	 */
+	@Override
+	public int read() throws IOException {
+		final int data = super.read();
+		if (data != -1) {
+			location++;
+			updateProgress();
+		}
+		return data;
+	}
+	
+	/**
+	 * Calls super and counts the number of read bytes. 
+	 * 
+	 * @see FilterInputStream
+	 */
+	@Override
+	public int read(byte[] b, int off, int len) throws IOException {
+		final int byteCount = super.read(b, off, len);
+		if (byteCount != -1) {
+			location += byteCount;
+			updateProgress();
+		}
+		return byteCount;
+	}
+	
+	/**
+	 * Registers listeners. Every registered listener will get informed if the file loading process makes any
+	 * progress.
+	 *  
+	 * @param ps The progress listener to be registered.
+	 */
+	public void addListener(InputStreamListener ps) {
+		listeners.add(ps);
+	}
+
+}
diff --git a/src/main/java/de/hft/stuttgart/citygml/viewer/parser/ParserConfiguration.java b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/ParserConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..219721090b2f3336182c6ffd973cf67a0e25c6cd
--- /dev/null
+++ b/src/main/java/de/hft/stuttgart/citygml/viewer/parser/ParserConfiguration.java
@@ -0,0 +1,78 @@
+/*-
+ * Copyright 2021 Hochschule für Technik Stuttgart
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.hft.stuttgart.citygml.viewer.parser;
+
+import java.io.Serializable;
+
+import org.locationtech.proj4j.BasicCoordinateTransform;
+import org.locationtech.proj4j.CRSFactory;
+import org.locationtech.proj4j.CoordinateReferenceSystem;
+
+/**
+ * Container class to store the configuration needed to parse a file. Also
+ * contains information on transformation of vertices.
+ * 
+ * @author Matthias Betz
+ *
+ */
+public class ParserConfiguration implements Serializable {
+
+	private static final long serialVersionUID = 6209047092991074661L;
+
+	private static final CRSFactory CRS_FACTORY = new CRSFactory();
+
+	private transient BasicCoordinateTransform targetTransform = null;
+	private transient BasicCoordinateTransform originalTransform = null;
+
+	private String targetTransformString;
+	private String originalTransformString;
+	private boolean hasTransformation = false;
+
+	public void setCoordinateSystem(CoordinateReferenceSystem crs, CoordinateReferenceSystem tgtCrs) {
+		if (crs != null && tgtCrs != null) {
+			hasTransformation = true;
+			targetTransformString = tgtCrs.getParameterString();
+			originalTransformString = crs.getParameterString();
+			targetTransform = new BasicCoordinateTransform(crs, tgtCrs);
+			originalTransform = new BasicCoordinateTransform(tgtCrs, crs);
+		}
+	}
+
+	public BasicCoordinateTransform getTargetTransform() {
+		if (hasTransformation && targetTransform == null) {
+			createCoordinateTransforms();
+		}
+		return targetTransform;
+	}
+
+	private void createCoordinateTransforms() {
+		CoordinateReferenceSystem tgtCrs = null;
+		CoordinateReferenceSystem crs = null;
+		synchronized (CRS_FACTORY) {
+			tgtCrs = CRS_FACTORY.createFromParameters("Target", targetTransformString);
+			crs = CRS_FACTORY.createFromParameters("Original", originalTransformString);
+		}
+		targetTransform = new BasicCoordinateTransform(crs, tgtCrs);
+		originalTransform = new BasicCoordinateTransform(tgtCrs, crs);
+	}
+
+	public BasicCoordinateTransform getOriginalTransform() {
+		if (hasTransformation && originalTransform == null) {
+			createCoordinateTransforms();
+		}
+		return originalTransform;
+	}
+}
diff --git a/src/main/resources/fragment.frag b/src/main/resources/fragment.frag
new file mode 100644
index 0000000000000000000000000000000000000000..771dc5959481f64139059787fa484517b3364b67
--- /dev/null
+++ b/src/main/resources/fragment.frag
@@ -0,0 +1,16 @@
+#version 330
+
+
+// Incoming interpolated (between vertices) color.
+in vec3 interpolatedColor;
+
+
+// Outgoing final color.
+layout (location = 0) out vec4 outputColor;
+
+
+void main()
+{
+    // We simply pad the interpolatedColor
+    outputColor = vec4(interpolatedColor, 1);
+}
\ No newline at end of file
diff --git a/src/main/resources/vertex.vert b/src/main/resources/vertex.vert
new file mode 100644
index 0000000000000000000000000000000000000000..c23f4a8a6ae345aba4b2d605839ecae8472d412c
--- /dev/null
+++ b/src/main/resources/vertex.vert
@@ -0,0 +1,25 @@
+#version 330
+
+
+// Incoming vertex position, Model Space.
+layout (location = 0) in vec3 position;
+
+// Incoming vertex color.
+layout (location = 1) in vec3 color;
+
+// Projection, view and model matrix.
+uniform mat4 projViewModel;
+
+
+// Outgoing color.
+out vec3 interpolatedColor;
+
+
+void main() {
+
+    // Normally gl_Position is in Clip Space and we calculate it by multiplying together all the matrices
+    gl_Position = projViewModel * vec4(position, 1);
+
+    // We assign the color to the outgoing variable.
+    interpolatedColor = color;
+}
\ No newline at end of file