/*-
 *  Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
 * 
 *  This file is part of CityDoctor2.
 *
 *  CityDoctor2 is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  CityDoctor2 is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with CityDoctor2.  If not, see <https://www.gnu.org/licenses/>.
 */
package de.hft.stuttgart.citydoctor2.mapper;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.citygml4j.model.citygml.bridge.AbstractBridge;
import org.citygml4j.model.citygml.bridge.Bridge;
import org.citygml4j.model.citygml.bridge.BridgePart;
import org.citygml4j.model.citygml.building.BuildingInstallationProperty;
import org.citygml4j.model.citygml.building.BuildingPartProperty;
import org.citygml4j.model.citygml.building.OpeningProperty;
import org.citygml4j.model.citygml.core.CityModel;
import org.citygml4j.model.citygml.landuse.LandUse;
import org.citygml4j.model.citygml.transportation.AuxiliaryTrafficArea;
import org.citygml4j.model.citygml.transportation.AuxiliaryTrafficAreaProperty;
import org.citygml4j.model.citygml.transportation.Railway;
import org.citygml4j.model.citygml.transportation.Road;
import org.citygml4j.model.citygml.transportation.Square;
import org.citygml4j.model.citygml.transportation.Track;
import org.citygml4j.model.citygml.transportation.TrafficArea;
import org.citygml4j.model.citygml.transportation.TrafficAreaProperty;
import org.citygml4j.model.citygml.transportation.TransportationComplex;
import org.citygml4j.model.citygml.vegetation.PlantCover;
import org.citygml4j.model.citygml.vegetation.SolitaryVegetationObject;
import org.citygml4j.model.citygml.waterbody.WaterBody;
import org.citygml4j.model.gml.geometry.AbstractGeometry;
import org.citygml4j.model.gml.geometry.GeometryProperty;
import org.citygml4j.model.gml.geometry.complexes.CompositeSurface;
import org.citygml4j.model.gml.geometry.primitives.AbstractSolid;
import org.citygml4j.util.walker.FeatureWalker;

import de.hft.stuttgart.citydoctor2.datastructure.AbstractBuilding;
import de.hft.stuttgart.citydoctor2.datastructure.BoundarySurface;
import de.hft.stuttgart.citydoctor2.datastructure.BoundarySurfaceType;
import de.hft.stuttgart.citydoctor2.datastructure.BridgeObject;
import de.hft.stuttgart.citydoctor2.datastructure.BridgeObject.BridgeType;
import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.BuildingInstallation;
import de.hft.stuttgart.citydoctor2.datastructure.BuildingPart;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.GmlId;
import de.hft.stuttgart.citydoctor2.datastructure.LandObject;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.LinkedPolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Opening;
import de.hft.stuttgart.citydoctor2.datastructure.OpeningType;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.SurfaceFeatureType;
import de.hft.stuttgart.citydoctor2.datastructure.TransportationObject;
import de.hft.stuttgart.citydoctor2.datastructure.TransportationObject.TransportationType;
import de.hft.stuttgart.citydoctor2.datastructure.Vegetation;
import de.hft.stuttgart.citydoctor2.datastructure.Vegetation.VegetationType;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.datastructure.WaterObject;
import de.hft.stuttgart.citydoctor2.math.graph.KDTree;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.utils.Localization;
import de.hft.stuttgart.citydoctor2.utils.Pair;

/**
 * This walker maps all cityGML4j features to the respective CityDoctor data
 * structure.
 * 
 * @author Matthias Betz
 *
 */
public class FeatureMapper extends FeatureWalker {

	private static final Logger logger = LogManager.getLogger(FeatureMapper.class);

	private CityDoctorModel model;
	private ParserConfiguration config;

	private double neighborDistance;

	public FeatureMapper(ParserConfiguration config, File file) {
		this.config = config;
		neighborDistance = 1.8d / Math.pow(10, config.getNumberOfRoundingPlaces());
		model = new CityDoctorModel(config, file);
	}

	@Override
	public void visit(WaterBody waterBody) {
		WaterObject wo = new WaterObject();
		model.addWater(wo);
		wo.setGmlObject(waterBody);
		if (waterBody.isSetId()) {
			wo.setGmlId(new GmlId(waterBody.getId()));
		}
		mapWaterBodyGeometries(waterBody, wo);
	}

	private void mapWaterBodyGeometries(WaterBody waterBody, WaterObject wo) {
		Map<String, ConcretePolygon> polygons = new HashMap<>();
		Map<Vertex, Vertex> vertexMap = new HashMap<>();
		List<Pair<String, Geometry>> linkedPolygons = new ArrayList<>();
		GeometryMapper.mapLod0MultiSurface(waterBody.getLod0MultiSurface(), wo, config, polygons, linkedPolygons,
				vertexMap);
		waterBody.unsetLod0MultiSurface();
		GeometryMapper.mapLod1MultiSurface(waterBody.getLod1MultiSurface(), wo, config, polygons, linkedPolygons,
				vertexMap);
		waterBody.unsetLod1MultiSurface();
		GeometryMapper.mapLod1Solid(waterBody.getLod1Solid(), wo, config, polygons, linkedPolygons, vertexMap);
		waterBody.unsetLod1Solid();
		GeometryMapper.mapLod2Solid(waterBody.getLod2Solid(), wo, config, polygons, linkedPolygons, vertexMap);
		waterBody.unsetLod2Solid();
		GeometryMapper.mapLod3Solid(waterBody.getLod3Solid(), wo, config, polygons, linkedPolygons, vertexMap);
		waterBody.unsetLod3Solid();
		GeometryMapper.mapLod4Solid(waterBody.getLod4Solid(), wo, config, polygons, linkedPolygons, vertexMap);
		waterBody.unsetLod4Solid();
		createLinkedPolygons(polygons, linkedPolygons);
		updateEdgesAndVertices(wo);
	}

	private void createLinkedPolygons(Map<String, ConcretePolygon> polygons,
			List<Pair<String, Geometry>> linkedPolygons) {
		for (Pair<String, Geometry> link : linkedPolygons) {
			ConcretePolygon concPoly = polygons.get(link.getValue0());
			if (concPoly == null) {
				if (logger.isWarnEnabled()) {
					logger.warn(Localization.getText("FeatureMapper.polygonUnreferenced"), link.getValue0());
				}
				continue;
			}
			Geometry geom = link.getValue1();
			LinkedPolygon lPoly = new LinkedPolygon(concPoly, geom);
			if (geom.getParent() instanceof BoundarySurface) {
				BoundarySurface bs = (BoundarySurface) geom.getParent();
				lPoly.setPartOfSurface(bs);
				if (bs.getParent() instanceof BuildingInstallation) {
					lPoly.setPartOfInstallation((BuildingInstallation) bs.getParent());
				}
			}
			geom.addPolygon(lPoly);
		}
	}

	private void updateEdgesAndVertices(CityObject co) {
		// searching for neighboring vertices, replacing them with one single vertex to
		// avoid later problems with edges and manifold problems
		for (Geometry geom : co.getGeometries()) {
			KDTree tree = new KDTree();
			for (Polygon poly : geom.getPolygons()) {
				LinearRing lr = poly.getExteriorRing();
				updateRing(tree, lr);
				for (LinearRing innerRing : poly.getInnerRings()) {
					updateRing(tree, innerRing);
				}
			}
			if (!config.useLowMemoryConsumption()) {
				// no low memory consumption mode meaning create all meta information in geometry
				geom.updateEdgesAndVertices();
			}
		}
	}

	private void updateRing(KDTree tree, LinearRing lr) {
		for (int i = 0; i < lr.getVertices().size(); i++) {
			Vertex v = lr.getVertices().get(i);
			List<Vertex> nodesInRange = tree.getNodesInRange(v, neighborDistance);
			if (nodesInRange.isEmpty()) {
				tree.add(v);
			} else {
				if (nodesInRange.size() != 1) {
					throw new IllegalStateException(
							"Found more than one vertex in range of " + v + " this should never happen here");
				}
				// replace other vertex with neighboring one
				Vertex original = nodesInRange.get(0);
				lr.setVertex(i, original);
			}
		}
	}

	@Override
	public void visit(PlantCover plantCover) {
		Vegetation veg = new Vegetation(VegetationType.PLANT_COVER);
		model.addVegetation(veg);
		mapPlantCover(plantCover, veg);
	}

	private void mapPlantCover(PlantCover plantCover, Vegetation veg) {
		veg.setGmlObject(plantCover);
		if (plantCover.isSetId()) {
			veg.setGmlId(new GmlId(plantCover.getId()));
		}
		mapPlantCoverGeometries(plantCover, veg);
	}

	private void mapPlantCoverGeometries(PlantCover plantCover, Vegetation veg) {
		Map<String, ConcretePolygon> polygons = new HashMap<>();
		List<Pair<String, Geometry>> linkedPolygons = new ArrayList<>();
		Map<Vertex, Vertex> vertexMap = new HashMap<>();
		GeometryMapper.mapLod1MultiSurface(plantCover.getLod1MultiSurface(), veg, config, polygons, linkedPolygons,
				vertexMap);
		plantCover.unsetLod1MultiSurface();
		GeometryMapper.mapLod2MultiSurface(plantCover.getLod2MultiSurface(), veg, config, polygons, linkedPolygons,
				vertexMap);
		plantCover.unsetLod2MultiSurface();
		GeometryMapper.mapLod3MultiSurface(plantCover.getLod3MultiSurface(), veg, config, polygons, linkedPolygons,
				vertexMap);
		plantCover.unsetLod3MultiSurface();
		GeometryMapper.mapLod4MultiSurface(plantCover.getLod4MultiSurface(), veg, config, polygons, linkedPolygons,
				vertexMap);
		plantCover.unsetLod4MultiSurface();
		createLinkedPolygons(polygons, linkedPolygons);
		updateEdgesAndVertices(veg);
	}

	@Override
	public void visit(SolitaryVegetationObject solitaryVegetationObject) {
		Vegetation veg = new Vegetation(VegetationType.SOLITARY_VEGETATION_OBJECT);
		model.addVegetation(veg);
		mapSolitaryVegObject(solitaryVegetationObject, veg);
	}

	private void mapSolitaryVegObject(SolitaryVegetationObject svo, Vegetation veg) {
		veg.setGmlObject(svo);
		if (svo.isSetId()) {
			veg.setGmlId(new GmlId(svo.getId()));
		}
		mapSolitaryVegGeometries(svo, veg);
	}

	private void mapSolitaryVegGeometries(SolitaryVegetationObject svo, Vegetation veg) {
		Map<String, ConcretePolygon> polygons = new HashMap<>();
		List<Pair<String, Geometry>> linkedPolygons = new ArrayList<>();
		Map<Vertex, Vertex> vertexMap = new HashMap<>();
		GeometryMapper.mapLod1Geometry(svo.getLod1Geometry(), veg, config, polygons, linkedPolygons, vertexMap);
		svo.unsetLod1Geometry();
		GeometryMapper.mapLod2Geometry(svo.getLod2Geometry(), veg, config, polygons, linkedPolygons, vertexMap);
		svo.unsetLod2Geometry();
		GeometryMapper.mapLod3Geometry(svo.getLod3Geometry(), veg, config, polygons, linkedPolygons, vertexMap);
		svo.unsetLod3Geometry();
		GeometryMapper.mapLod4Geometry(svo.getLod4Geometry(), veg, config, polygons, linkedPolygons, vertexMap);
		svo.unsetLod4Geometry();
		createLinkedPolygons(polygons, linkedPolygons);
		updateEdgesAndVertices(veg);
	}

	@Override
	public void visit(LandUse landUse) {
		LandObject lo = new LandObject(landUse);
		model.addLand(lo);
		mapLandUse(landUse, lo);
	}

	private void mapLandUse(LandUse landUse, LandObject lo) {
		lo.setGmlObject(landUse);
		if (landUse.isSetId()) {
			lo.setGmlId(new GmlId(landUse.getId()));
		}
		mapLandUseGeometries(landUse, lo);
	}

	private void mapLandUseGeometries(LandUse landUse, LandObject lo) {
		Map<String, ConcretePolygon> polygons = new HashMap<>();
		List<Pair<String, Geometry>> linkedPolygons = new ArrayList<>();
		Map<Vertex, Vertex> vertexMap = new HashMap<>();
		GeometryMapper.mapLod0MultiSurface(landUse.getLod0MultiSurface(), lo, config, polygons, linkedPolygons,
				vertexMap);
		landUse.unsetLod0MultiSurface();
		GeometryMapper.mapLod1MultiSurface(landUse.getLod1MultiSurface(), lo, config, polygons, linkedPolygons,
				vertexMap);
		landUse.unsetLod1MultiSurface();
		GeometryMapper.mapLod2MultiSurface(landUse.getLod2MultiSurface(), lo, config, polygons, linkedPolygons,
				vertexMap);
		landUse.unsetLod2MultiSurface();
		GeometryMapper.mapLod3MultiSurface(landUse.getLod3MultiSurface(), lo, config, polygons, linkedPolygons,
				vertexMap);
		landUse.unsetLod3MultiSurface();
		GeometryMapper.mapLod4MultiSurface(landUse.getLod4MultiSurface(), lo, config, polygons, linkedPolygons,
				vertexMap);
		landUse.unsetLod4MultiSurface();
		createLinkedPolygons(polygons, linkedPolygons);
		updateEdgesAndVertices(lo);
	}

	@Override
	public void visit(Road road) {
		TransportationObject trans = new TransportationObject(TransportationType.ROAD);
		mapTransportationComplex(road, trans);
	}

	@Override
	public void visit(Railway railway) {
		TransportationObject trans = new TransportationObject(TransportationType.RAILWAY);
		mapTransportationComplex(railway, trans);
	}

	@Override
	public void visit(Square square) {
		TransportationObject trans = new TransportationObject(TransportationType.SQUARE);
		mapTransportationComplex(square, trans);
	}

	@Override
	public void visit(Track track) {
		TransportationObject trans = new TransportationObject(TransportationType.TRACK);
		mapTransportationComplex(track, trans);
	}

	@Override
	public void visit(AuxiliaryTrafficArea auxiliaryTrafficArea) {
		TransportationObject trans = new TransportationObject(TransportationType.AUXILLIARY_TRAFFIC_AREA);
		mapAuxTrafficArea(auxiliaryTrafficArea, trans);
	}

	@Override
	public void visit(TransportationComplex tc) {
		TransportationObject trans = new TransportationObject(TransportationType.TRANSPORTATION_COMPLEX);
		model.addTransportation(trans);
		mapTransportationComplex(tc, trans);

		if (tc.isSetAuxiliaryTrafficArea()) {
			for (AuxiliaryTrafficAreaProperty atap : tc.getAuxiliaryTrafficArea()) {
				if (atap.isSetAuxiliaryTrafficArea()) {
					TransportationObject subTrans = new TransportationObject(
							TransportationType.AUXILLIARY_TRAFFIC_AREA);
					trans.addChild(subTrans);
					mapAuxTrafficArea(atap.getAuxiliaryTrafficArea(), subTrans);
				}
			}
		}

		if (tc.isSetTrafficArea()) {
			for (TrafficAreaProperty tap : tc.getTrafficArea()) {
				if (tap.isSetTrafficArea()) {
					TransportationObject subTrans = new TransportationObject(TransportationType.TRAFFIC_AREA);
					trans.addChild(subTrans);
					mapTrafficArea(tap.getTrafficArea(), subTrans);
				}
			}
		}
	}

	@Override
	public void visit(TrafficArea trafficArea) {
		TransportationObject trans = new TransportationObject(TransportationType.TRAFFIC_AREA);
		model.addTransportation(trans);
		mapTrafficArea(trafficArea, trans);
	}

	private void mapTrafficArea(TrafficArea ta, TransportationObject to) {
		to.setGmlObject(ta);
		if (ta.isSetId()) {
			to.setGmlId(new GmlId(ta.getId()));
		}
		mapTrafficAreaGeometries(ta, to);
	}

	private void mapTrafficAreaGeometries(TrafficArea ta, TransportationObject to) {
		Map<String, ConcretePolygon> polygons = new HashMap<>();
		List<Pair<String, Geometry>> linkedPolygons = new ArrayList<>();
		Map<Vertex, Vertex> vertexMap = new HashMap<>();
		GeometryMapper.mapLod2MultiSurface(ta.getLod2MultiSurface(), to, config, polygons, linkedPolygons, vertexMap);
		ta.unsetLod2MultiSurface();
		GeometryMapper.mapLod3MultiSurface(ta.getLod3MultiSurface(), to, config, polygons, linkedPolygons, vertexMap);
		ta.unsetLod3MultiSurface();
		GeometryMapper.mapLod4MultiSurface(ta.getLod4MultiSurface(), to, config, polygons, linkedPolygons, vertexMap);
		ta.unsetLod4MultiSurface();
		createLinkedPolygons(polygons, linkedPolygons);
		updateEdgesAndVertices(to);
	}

	private void mapAuxTrafficArea(AuxiliaryTrafficArea ata, TransportationObject to) {
		to.setGmlObject(ata);
		if (ata.isSetId()) {
			to.setGmlId(new GmlId(ata.getId()));
		}
		mapAuxTrafficAreaGeometries(ata, to);
	}

	private void mapAuxTrafficAreaGeometries(AuxiliaryTrafficArea ata, TransportationObject to) {
		Map<String, ConcretePolygon> polygons = new HashMap<>();
		List<Pair<String, Geometry>> linkedPolygons = new ArrayList<>();
		Map<Vertex, Vertex> vertexMap = new HashMap<>();
		GeometryMapper.mapLod2MultiSurface(ata.getLod2MultiSurface(), to, config, polygons, linkedPolygons, vertexMap);
		ata.unsetLod2MultiSurface();
		GeometryMapper.mapLod3MultiSurface(ata.getLod3MultiSurface(), to, config, polygons, linkedPolygons, vertexMap);
		ata.unsetLod3MultiSurface();
		GeometryMapper.mapLod4MultiSurface(ata.getLod4MultiSurface(), to, config, polygons, linkedPolygons, vertexMap);
		ata.unsetLod4MultiSurface();
		createLinkedPolygons(polygons, linkedPolygons);
		updateEdgesAndVertices(to);
	}

	private void mapTransportationComplex(TransportationComplex tc, TransportationObject to) {
		model.addTransportation(to);
		to.setGmlObject(tc);
		if (tc.isSetId()) {
			to.setGmlId(new GmlId(tc.getId()));
		}
		mapTransportationComplexGeometries(tc, to);
	}

	private void mapTransportationComplexGeometries(TransportationComplex tc, TransportationObject to) {
		Map<String, ConcretePolygon> polygons = new HashMap<>();
		List<Pair<String, Geometry>> linkedPolygons = new ArrayList<>();
		Map<Vertex, Vertex> vertexMap = new HashMap<>();
		GeometryMapper.mapLod1MultiSurface(tc.getLod1MultiSurface(), to, config, polygons, linkedPolygons, vertexMap);
		tc.unsetLod1MultiSurface();
		GeometryMapper.mapLod2MultiSurface(tc.getLod2MultiSurface(), to, config, polygons, linkedPolygons, vertexMap);
		tc.unsetLod2MultiSurface();
		GeometryMapper.mapLod3MultiSurface(tc.getLod3MultiSurface(), to, config, polygons, linkedPolygons, vertexMap);
		tc.unsetLod3MultiSurface();
		GeometryMapper.mapLod4MultiSurface(tc.getLod4MultiSurface(), to, config, polygons, linkedPolygons, vertexMap);
		tc.unsetLod4MultiSurface();
		createLinkedPolygons(polygons, linkedPolygons);
		updateEdgesAndVertices(to);
	}

	@Override
	public void visit(org.citygml4j.model.citygml.building.Building building) {
		Building coBuilding = new Building();
		model.addBuilding(coBuilding);
		mapAbstractBuilding(building, coBuilding);
		if (building.isSetConsistsOfBuildingPart()) {
			for (BuildingPartProperty bpp : building.getConsistsOfBuildingPart()) {
				if (bpp.isSetBuildingPart()) {
					BuildingPart coBuildingPart = new BuildingPart(coBuilding);
					mapAbstractBuilding(bpp.getBuildingPart(), coBuildingPart);
				}
			}
		}
	}

	private void mapAbstractBuilding(org.citygml4j.model.citygml.building.AbstractBuilding ab,
			AbstractBuilding coBuilding) {
		coBuilding.setGmlObject(ab);
		if (ab.isSetId()) {
			coBuilding.setGmlId(new GmlId(ab.getId()));
		}

		Map<String, ConcretePolygon> polygons = new HashMap<>();
		List<Pair<String, Geometry>> linkedPolygons = new ArrayList<>();
		Map<Vertex, Vertex> vertexMap = new HashMap<>();

		mapBuildingMultiSurfaceGeometries(ab, coBuilding, polygons, linkedPolygons, vertexMap);
		mapBuildingSolidGeometries(ab, coBuilding, polygons, linkedPolygons, vertexMap);

		// handle boundary surfaces
		if (ab.isSetBoundedBySurface()) {
			for (org.citygml4j.model.citygml.building.BoundarySurfaceProperty bsp : ab.getBoundedBySurface()) {
				mapBuildingSurface(bsp, coBuilding, polygons, linkedPolygons, vertexMap);
			}
		}

		// handle outer building installations
		if (ab.isSetOuterBuildingInstallation()) {
			for (BuildingInstallationProperty bip : ab.getOuterBuildingInstallation()) {
				mapBuildingInstallation(bip, coBuilding, polygons, linkedPolygons, vertexMap);
			}
		}
		createLinkedPolygons(polygons, linkedPolygons);
		updateEdgesAndVertices(coBuilding);
	}

	private void mapBuildingInstallation(BuildingInstallationProperty bip, AbstractBuilding coBuilding,
			Map<String, ConcretePolygon> polygons, List<Pair<String, Geometry>> linkedPolygons,
			Map<Vertex, Vertex> vertexMap) {
		if (!bip.isSetBuildingInstallation()) {
			return;
		}
		BuildingInstallation coBi = new BuildingInstallation();
		coBuilding.addBuildingInstallation(coBi);
		org.citygml4j.model.citygml.building.BuildingInstallation gmlBi = bip.getBuildingInstallation();
		coBi.setGmlObject(gmlBi);

		if (gmlBi.isSetLod2Geometry()) {
			GeometryProperty<? extends AbstractGeometry> abstractGeometry = gmlBi.getLod2Geometry();
			GeometryType type = extractGeometryType(abstractGeometry);
			Geometry geom = new Geometry(type, Lod.LOD2);
			GeometryMapper mapper = new GeometryMapper(geom, config, polygons, linkedPolygons, vertexMap);
			GeometryMapper.mapBuildingInstallationIntoGeometry(abstractGeometry, mapper, coBi);
			coBi.addGeometry(geom);
			gmlBi.unsetLod2Geometry();
		}
		if (gmlBi.isSetLod3Geometry()) {
			GeometryProperty<? extends AbstractGeometry> abstractGeometry = gmlBi.getLod3Geometry();
			GeometryType type = extractGeometryType(abstractGeometry);
			Geometry geom = new Geometry(type, Lod.LOD3);
			GeometryMapper mapper = new GeometryMapper(geom, config, polygons, linkedPolygons, vertexMap);
			GeometryMapper.mapBuildingInstallationIntoGeometry(abstractGeometry, mapper, coBi);
			coBi.addGeometry(geom);
			gmlBi.unsetLod3Geometry();
		}
		if (gmlBi.isSetLod4Geometry()) {
			GeometryProperty<? extends AbstractGeometry> abstractGeometry = gmlBi.getLod4Geometry();
			GeometryType type = extractGeometryType(abstractGeometry);
			Geometry geom = new Geometry(type, Lod.LOD4);
			GeometryMapper mapper = new GeometryMapper(geom, config, polygons, linkedPolygons, vertexMap);
			GeometryMapper.mapBuildingInstallationIntoGeometry(abstractGeometry, mapper, coBi);
			coBi.addGeometry(geom);
			gmlBi.unsetLod4Geometry();
		}

		// handle boundary surfaces
		if (gmlBi.isSetBoundedBySurface()) {
			for (org.citygml4j.model.citygml.building.BoundarySurfaceProperty bsp : gmlBi.getBoundedBySurface()) {
				mapBuildingInstallationSurface(bsp, coBi, polygons, linkedPolygons, vertexMap);
			}
		}
		updateEdgesAndVertices(coBi);
	}

	private GeometryType extractGeometryType(GeometryProperty<? extends AbstractGeometry> agp) {
		AbstractGeometry abstractGeometry = agp.getObject();
		if (abstractGeometry instanceof CompositeSurface) {
			return GeometryType.COMPOSITE_SURFACE;
		} else if (abstractGeometry instanceof AbstractSolid) {
			return GeometryType.SOLID;
		}
		return GeometryType.MULTI_SURFACE;
	}

	private void mapBuildingSolidGeometries(org.citygml4j.model.citygml.building.AbstractBuilding ab,
			AbstractBuilding coBuilding, Map<String, ConcretePolygon> polygons,
			List<Pair<String, Geometry>> linkedPolygons, Map<Vertex, Vertex> vertexMap) {
		GeometryMapper.mapLod1Solid(ab.getLod1Solid(), coBuilding, config, polygons, linkedPolygons, vertexMap);
		ab.unsetLod1Solid();
		GeometryMapper.mapLod2Solid(ab.getLod2Solid(), coBuilding, config, polygons, linkedPolygons, vertexMap);
		ab.unsetLod2Solid();
		GeometryMapper.mapLod3Solid(ab.getLod3Solid(), coBuilding, config, polygons, linkedPolygons, vertexMap);
		ab.unsetLod3Solid();
		GeometryMapper.mapLod4Solid(ab.getLod4Solid(), coBuilding, config, polygons, linkedPolygons, vertexMap);
		ab.unsetLod4Solid();
	}

	private void mapBuildingMultiSurfaceGeometries(org.citygml4j.model.citygml.building.AbstractBuilding ab,
			AbstractBuilding coBuilding, Map<String, ConcretePolygon> polygons,
			List<Pair<String, Geometry>> linkedPolygons, Map<Vertex, Vertex> vertexMap) {
		GeometryMapper.mapLod1MultiSurface(ab.getLod1MultiSurface(), coBuilding, config, polygons, linkedPolygons,
				vertexMap);
		ab.unsetLod1MultiSurface();
		GeometryMapper.mapLod2MultiSurface(ab.getLod2MultiSurface(), coBuilding, config, polygons, linkedPolygons,
				vertexMap);
		ab.unsetLod2MultiSurface();
		GeometryMapper.mapLod3MultiSurface(ab.getLod3MultiSurface(), coBuilding, config, polygons, linkedPolygons,
				vertexMap);
		ab.unsetLod3MultiSurface();
		GeometryMapper.mapLod4MultiSurface(ab.getLod4MultiSurface(), coBuilding, config, polygons, linkedPolygons,
				vertexMap);
		ab.unsetLod4MultiSurface();
	}

	private void mapBuildingSurface(org.citygml4j.model.citygml.building.BoundarySurfaceProperty bsp,
			AbstractBuilding coBuilding, Map<String, ConcretePolygon> polygons,
			List<Pair<String, Geometry>> linkedPolygons, Map<Vertex, Vertex> vertexMap) {
		if (!bsp.isSetBoundarySurface()) {
			return;
		}
		org.citygml4j.model.citygml.building.AbstractBoundarySurface abs = bsp.getBoundarySurface();
		BoundarySurfaceType surfaceType = mapSurfaceType(abs);
		BoundarySurface bs = new BoundarySurface(SurfaceFeatureType.BUILDING, surfaceType, abs);
		if (abs.isSetId()) {
			bs.setGmlId(new GmlId(abs.getId()));
		}
		coBuilding.addBoundarySurface(bs);
		createBoundarySurfaceGeometries(abs, bs, null, polygons, linkedPolygons, vertexMap);
		mapBuildingOpenings(abs.getOpening(), bs, polygons, linkedPolygons, vertexMap);
		updateEdgesAndVertices(bs);
	}

	private void mapBuildingOpenings(List<OpeningProperty> openings, BoundarySurface bs,
			Map<String, ConcretePolygon> polygons, List<Pair<String, Geometry>> linkedPolygons,
			Map<Vertex, Vertex> vertexMap) {
		if (openings == null || openings.isEmpty()) {
			return;
		}
		for (org.citygml4j.model.citygml.building.OpeningProperty op : openings) {
			if (!op.isSetOpening()) {
				continue;
			}
			org.citygml4j.model.citygml.building.AbstractOpening ap = op.getOpening();
			OpeningType type = mapOpeningType(ap);
			Opening opening = new Opening(type, SurfaceFeatureType.BUILDING, bs, ap);
			bs.addOpening(opening);
			createOpeningGeometries(ap, opening, polygons, linkedPolygons, vertexMap);
			updateEdgesAndVertices(opening);
		}
	}

	private void createOpeningGeometries(org.citygml4j.model.citygml.building.AbstractOpening ap, Opening opening,
			Map<String, ConcretePolygon> polygons, List<Pair<String, Geometry>> linkedPolygons,
			Map<Vertex, Vertex> vertexMap) {
		if (ap.isSetLod3MultiSurface()) {
			GeometryMapper.mapLod3MultiSurface(ap.getLod3MultiSurface(), opening, config, polygons, linkedPolygons,
					vertexMap);
			ap.unsetLod3MultiSurface();
		}
		if (ap.isSetLod4MultiSurface()) {
			GeometryMapper.mapLod4MultiSurface(ap.getLod3MultiSurface(), opening, config, polygons, linkedPolygons,
					vertexMap);
			ap.unsetLod4MultiSurface();
		}
	}

	private OpeningType mapOpeningType(org.citygml4j.model.citygml.building.AbstractOpening ap) {
		if (ap instanceof org.citygml4j.model.citygml.building.Door) {
			return OpeningType.DOOR;
		}
		if (ap instanceof org.citygml4j.model.citygml.building.Window) {
			return OpeningType.WINDOW;
		}
		return OpeningType.UNKNOWN;
	}

	private void mapBuildingInstallationSurface(org.citygml4j.model.citygml.building.BoundarySurfaceProperty bsp,
			BuildingInstallation coBi, Map<String, ConcretePolygon> polygons,
			List<Pair<String, Geometry>> linkedPolygons, Map<Vertex, Vertex> vertexMap) {
		if (!bsp.isSetBoundarySurface()) {
			return;
		}
		org.citygml4j.model.citygml.building.AbstractBoundarySurface abs = bsp.getBoundarySurface();
		BoundarySurfaceType surfaceType = mapSurfaceType(abs);
		BoundarySurface bs = new BoundarySurface(SurfaceFeatureType.BUILDING, surfaceType, abs);
		if (abs.isSetId()) {
			bs.setGmlId(new GmlId(abs.getId()));
		}
		coBi.addBoundarySurface(bs);
		createBoundarySurfaceGeometries(abs, bs, coBi, polygons, linkedPolygons, vertexMap);
		mapBuildingOpenings(abs.getOpening(), bs, polygons, linkedPolygons, vertexMap);
		updateEdgesAndVertices(bs);
	}

	private void createBoundarySurfaceGeometries(org.citygml4j.model.citygml.building.AbstractBoundarySurface abs,
			BoundarySurface bs, BuildingInstallation coBi, Map<String, ConcretePolygon> polygons,
			List<Pair<String, Geometry>> linkedPolygons, Map<Vertex, Vertex> vertexMap) {
		if (abs.isSetLod2MultiSurface()) {
			Geometry geom = new Geometry(GeometryType.MULTI_SURFACE, Lod.LOD2);
			GeometryMapper mapper = new GeometryMapper(geom, config, polygons, linkedPolygons, vertexMap);
			GeometryMapper.mapBoundarySurfaceIntoGeometry(abs.getLod2MultiSurface(), mapper, bs, coBi);
			bs.addGeometry(geom);
			abs.unsetLod2MultiSurface();
		}
		if (abs.isSetLod3MultiSurface()) {
			Geometry geom = new Geometry(GeometryType.MULTI_SURFACE, Lod.LOD3);
			GeometryMapper mapper = new GeometryMapper(geom, config, polygons, linkedPolygons, vertexMap);
			GeometryMapper.mapBoundarySurfaceIntoGeometry(abs.getLod3MultiSurface(), mapper, bs, coBi);
			bs.addGeometry(geom);
			abs.unsetLod3MultiSurface();
		}
		if (abs.isSetLod4MultiSurface()) {
			Geometry geom = new Geometry(GeometryType.MULTI_SURFACE, Lod.LOD4);
			GeometryMapper mapper = new GeometryMapper(geom, config, polygons, linkedPolygons, vertexMap);
			GeometryMapper.mapBoundarySurfaceIntoGeometry(abs.getLod4MultiSurface(), mapper, bs, coBi);
			bs.addGeometry(geom);
			abs.unsetLod4MultiSurface();
		}
	}

	private void createBoundarySurfaceGeometries(org.citygml4j.model.citygml.bridge.AbstractBoundarySurface abs,
			BoundarySurface bs, Map<String, ConcretePolygon> polygons, List<Pair<String, Geometry>> linkedPolygons,
			Map<Vertex, Vertex> vertexMap) {
		if (abs.isSetLod2MultiSurface()) {
			Geometry geom = new Geometry(GeometryType.MULTI_SURFACE, Lod.LOD2);
			GeometryMapper mapper = new GeometryMapper(geom, config, polygons, linkedPolygons, vertexMap);
			GeometryMapper.mapBoundarySurfaceIntoGeometry(abs.getLod2MultiSurface(), mapper, bs, null);
			bs.addGeometry(geom);
			abs.unsetLod2MultiSurface();
		}
		if (abs.isSetLod3MultiSurface()) {
			Geometry geom = new Geometry(GeometryType.MULTI_SURFACE, Lod.LOD3);
			GeometryMapper mapper = new GeometryMapper(geom, config, polygons, linkedPolygons, vertexMap);
			GeometryMapper.mapBoundarySurfaceIntoGeometry(abs.getLod3MultiSurface(), mapper, bs, null);
			bs.addGeometry(geom);
			abs.unsetLod3MultiSurface();
		}
		if (abs.isSetLod4MultiSurface()) {
			Geometry geom = new Geometry(GeometryType.MULTI_SURFACE, Lod.LOD4);
			GeometryMapper mapper = new GeometryMapper(geom, config, polygons, linkedPolygons, vertexMap);
			GeometryMapper.mapBoundarySurfaceIntoGeometry(abs.getLod4MultiSurface(), mapper, bs, null);
			bs.addGeometry(geom);
			abs.unsetLod4MultiSurface();
		}
	}

	private BoundarySurfaceType mapSurfaceType(org.citygml4j.model.citygml.building.AbstractBoundarySurface abs) {
		switch (abs.getCityGMLClass()) {
		case BUILDING_WALL_SURFACE:
			return BoundarySurfaceType.WALL;
		case BUILDING_ROOF_SURFACE:
			return BoundarySurfaceType.ROOF;
		case BUILDING_GROUND_SURFACE:
			return BoundarySurfaceType.GROUND;
		case BUILDING_CLOSURE_SURFACE:
			return BoundarySurfaceType.CLOSURE;
		case BUILDING_FLOOR_SURFACE:
			return BoundarySurfaceType.OUTER_FLOOR;
		case OUTER_BUILDING_FLOOR_SURFACE:
			return BoundarySurfaceType.OUTER_FLOOR;
		case BUILDING_CEILING_SURFACE:
			return BoundarySurfaceType.OUTER_CEILING;
		case OUTER_BUILDING_CEILING_SURFACE:
			return BoundarySurfaceType.OUTER_CEILING;
		default:
			return BoundarySurfaceType.UNDEFINED;
		}
	}

	@Override
	public void visit(Bridge bridge) {
		BridgeObject coBridge = new BridgeObject(BridgeType.BRIDGE, bridge);
		mapBridge(bridge, coBridge);
	}

	@Override
	public void visit(BridgePart bridgePart) {
		BridgeObject coBridge = new BridgeObject(BridgeType.BRIDGE_PART, bridgePart);
		mapBridge(bridgePart, coBridge);
	}

	private void mapBridge(AbstractBridge aBridge, BridgeObject coBridge) {
		model.addBridge(coBridge);
		coBridge.setGmlObject(aBridge);
		if (aBridge.isSetId()) {
			coBridge.setGmlId(new GmlId(aBridge.getId()));
		}

		Map<String, ConcretePolygon> polygons = new HashMap<>();
		List<Pair<String, Geometry>> linkedPolygons = new ArrayList<>();
		Map<Vertex, Vertex> vertexMap = new HashMap<>();

		// map geometries
		mapBridgeMultiSurfaceGeometries(aBridge, coBridge, polygons, linkedPolygons, vertexMap);
		mapBridgeSolidGeometries(aBridge, coBridge, polygons, linkedPolygons, vertexMap);

		// handle boundary surfaces
		if (aBridge.isSetBoundedBySurface()) {
			for (org.citygml4j.model.citygml.bridge.BoundarySurfaceProperty bsp : aBridge.getBoundedBySurface()) {
				mapBridgeSurface(bsp, coBridge, polygons, linkedPolygons, vertexMap);
			}
		}
		createLinkedPolygons(polygons, linkedPolygons);
		updateEdgesAndVertices(coBridge);
	}

	private void mapBridgeMultiSurfaceGeometries(AbstractBridge aBridge, BridgeObject coBridge,
			Map<String, ConcretePolygon> polygons, List<Pair<String, Geometry>> linkedPolygons,
			Map<Vertex, Vertex> vertexMap) {
		GeometryMapper.mapLod1MultiSurface(aBridge.getLod1MultiSurface(), coBridge, config, polygons, linkedPolygons,
				vertexMap);
		aBridge.unsetLod1MultiSurface();
		GeometryMapper.mapLod2MultiSurface(aBridge.getLod2MultiSurface(), coBridge, config, polygons, linkedPolygons,
				vertexMap);
		aBridge.unsetLod2MultiSurface();
		GeometryMapper.mapLod3MultiSurface(aBridge.getLod3MultiSurface(), coBridge, config, polygons, linkedPolygons,
				vertexMap);
		aBridge.unsetLod3MultiSurface();
		GeometryMapper.mapLod4MultiSurface(aBridge.getLod4MultiSurface(), coBridge, config, polygons, linkedPolygons,
				vertexMap);
		aBridge.unsetLod4MultiSurface();
	}

	private void mapBridgeSolidGeometries(AbstractBridge aBridge, BridgeObject coBridge,
			Map<String, ConcretePolygon> polygons, List<Pair<String, Geometry>> linkedPolygons,
			Map<Vertex, Vertex> vertexMap) {
		GeometryMapper.mapLod1Solid(aBridge.getLod1Solid(), coBridge, config, polygons, linkedPolygons, vertexMap);
		aBridge.unsetLod1Solid();

		GeometryMapper.mapLod2Solid(aBridge.getLod2Solid(), coBridge, config, polygons, linkedPolygons, vertexMap);
		aBridge.unsetLod2Solid();

		GeometryMapper.mapLod3Solid(aBridge.getLod3Solid(), coBridge, config, polygons, linkedPolygons, vertexMap);
		aBridge.unsetLod3Solid();

		GeometryMapper.mapLod4Solid(aBridge.getLod4Solid(), coBridge, config, polygons, linkedPolygons, vertexMap);
		aBridge.unsetLod4Solid();
	}

	private void mapBridgeSurface(org.citygml4j.model.citygml.bridge.BoundarySurfaceProperty bsp, BridgeObject coBridge,
			Map<String, ConcretePolygon> polygons, List<Pair<String, Geometry>> linkedPolygons,
			Map<Vertex, Vertex> vertexMap) {
		if (!bsp.isSetBoundarySurface()) {
			return;
		}
		org.citygml4j.model.citygml.bridge.AbstractBoundarySurface abs = bsp.getBoundarySurface();
		BoundarySurfaceType surfaceType = mapSurfaceType(abs);
		BoundarySurface bs = new BoundarySurface(SurfaceFeatureType.BRIDGE, surfaceType, abs);
		bs.setGmlObject(abs);
		coBridge.addBoundarySurface(bs);
		mapBridgeOpenings(abs.getOpening(), bs, polygons, linkedPolygons, vertexMap);
		createBoundarySurfaceGeometries(abs, bs, polygons, linkedPolygons, vertexMap);
		updateEdgesAndVertices(bs);
	}

	private void mapBridgeOpenings(List<org.citygml4j.model.citygml.bridge.OpeningProperty> openings,
			BoundarySurface bs, Map<String, ConcretePolygon> polygons, List<Pair<String, Geometry>> linkedPolygons,
			Map<Vertex, Vertex> vertexMap) {
		if (openings == null || openings.isEmpty()) {
			return;
		}
		for (org.citygml4j.model.citygml.bridge.OpeningProperty op : openings) {
			if (!op.isSetOpening()) {
				continue;
			}
			org.citygml4j.model.citygml.bridge.AbstractOpening ap = op.getOpening();
			OpeningType type = mapOpeningType(ap);
			Opening opening = new Opening(type, SurfaceFeatureType.BRIDGE, bs, ap);
			bs.addOpening(opening);
			createOpeningGeometries(ap, opening, polygons, linkedPolygons, vertexMap);
			updateEdgesAndVertices(opening);
		}
	}

	private void createOpeningGeometries(org.citygml4j.model.citygml.bridge.AbstractOpening ap, Opening opening,
			Map<String, ConcretePolygon> polygons, List<Pair<String, Geometry>> linkedPolygons,
			Map<Vertex, Vertex> vertexMap) {
		if (ap.isSetLod3MultiSurface()) {
			GeometryMapper.mapLod3MultiSurface(ap.getLod3MultiSurface(), opening, config, polygons, linkedPolygons,
					vertexMap);
			ap.unsetLod3MultiSurface();
		}
		if (ap.isSetLod4MultiSurface()) {
			GeometryMapper.mapLod4MultiSurface(ap.getLod3MultiSurface(), opening, config, polygons, linkedPolygons,
					vertexMap);
			ap.unsetLod4MultiSurface();
		}
	}

	private OpeningType mapOpeningType(org.citygml4j.model.citygml.bridge.AbstractOpening ap) {
		if (ap instanceof org.citygml4j.model.citygml.bridge.Door) {
			return OpeningType.DOOR;
		}
		if (ap instanceof org.citygml4j.model.citygml.bridge.Window) {
			return OpeningType.WINDOW;
		}
		return OpeningType.UNKNOWN;
	}

	private BoundarySurfaceType mapSurfaceType(org.citygml4j.model.citygml.bridge.AbstractBoundarySurface abs) {
		switch (abs.getCityGMLClass()) {
		case BRIDGE_CEILING_SURFACE:
			return BoundarySurfaceType.CEILING;
		case BRIDGE_CLOSURE_SURFACE:
			return BoundarySurfaceType.CLOSURE;
		case BRIDGE_FLOOR_SURFACE:
			return BoundarySurfaceType.FLOOR;
		case BRIDGE_GROUND_SURFACE:
			return BoundarySurfaceType.GROUND;
		case BRIDGE_ROOF_SURFACE:
			return BoundarySurfaceType.ROOF;
		case BRIDGE_WALL_SURFACE:
			return BoundarySurfaceType.WALL;
		case INTERIOR_BRIDGE_WALL_SURFACE:
			return BoundarySurfaceType.INTERIOR_WALL;
		case OUTER_BRIDGE_CEILING_SURFACE:
			return BoundarySurfaceType.OUTER_CEILING;
		case OUTER_BUILDING_FLOOR_SURFACE:
			return BoundarySurfaceType.OUTER_FLOOR;
		default:
			return BoundarySurfaceType.UNDEFINED;
		}
	}

	public CityDoctorModel getModel() {
		return model;
	}

	public void setCityModel(CityModel cModel) {
		model.setCityModel(cModel);
	}

}
