/*-
* 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 .
*/
package de.hft.stuttgart.citydoctor2.reporting.pdf;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import de.hft.stuttgart.citydoctor2.check.RequirementConfiguration;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckId;
import de.hft.stuttgart.citydoctor2.check.CheckResult;
import de.hft.stuttgart.citydoctor2.check.ErrorId;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration;
import de.hft.stuttgart.citydoctor2.checkresult.utility.CheckReportWriteException;
import de.hft.stuttgart.citydoctor2.datastructure.BoundarySurface;
import de.hft.stuttgart.citydoctor2.datastructure.BridgeObject;
import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.BuildingPart;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.LandObject;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Opening;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.TransportationObject;
import de.hft.stuttgart.citydoctor2.datastructure.Vegetation;
import de.hft.stuttgart.citydoctor2.datastructure.WaterObject;
import de.hft.stuttgart.citydoctor2.reporting.StreamReporter;
import de.hft.stuttgart.citydoctor2.utils.Localization;
/**
* Reporter to create a PDF report out of a stream of feature results
*
* @author Matthias Betz
*
*/
public class PdfStreamReporter implements StreamReporter {
private static final Logger logger = LogManager.getLogger(PdfStreamReporter.class);
private static final String ERROR_COLOR = "red";
@SuppressWarnings("unused")
private static final String WARNING_COLOR = "yellow";
private static final String OK_COLOR = "green";
private PdfReport report;
private OutputStream outFile;
private Map errorStatistics;
private ValidationConfiguration config;
private Section statistics;
private Section vr;
private Section buildings;
private int numErrorBuildings;
private int numOkBuildings;
private Section vegetation;
private int numErrorVegetation;
private int numOkVegetation;
private Section trans;
private int numErrorTrans;
private int numOkTrans;
private Section bridge;
private int numErrorBridge;
private int numOkBridge;
private Section water;
private int numErrorWater;
private int numOkWater;
private Section land;
private int numErrorLand;
private int numOkLand;
private Section globalErrors;
private Map sectionMap = new HashMap<>();
public PdfStreamReporter(OutputStream pdfOutputFile, String fileName, ValidationConfiguration config) {
this.config = config;
errorStatistics = new HashMap<>();
outFile = pdfOutputFile;
report = new PdfReport();
report.writeSourceFileName(fileName);
writeEnvironment();
statistics = report.createSection("Statistics", true);
statistics.addTextElement("Object distribution:");
writeValidationPlan();
vr = report.createSection("Validation Results", true);
}
private void writeValidationPlan() {
Section vp = report.createSection("Validation Plan", true);
vp.addTextElement("numberOfRoundingPlaces = " + config.getNumberOfRoundingPlaces());
vp.addTextElement("minVertexDistance = " + config.getMinVertexDistance());
if (config.getSchematronFilePath() != null && !config.getSchematronFilePath().isEmpty()) {
vp.addTextElement("Schematron file: " + config.getSchematronFilePath());
}
for (Entry e : config.getRequirements().entrySet()) {
String text = e.getKey();
String color;
if (e.getValue().isEnabled()) {
text = text + " = enabled";
color = OK_COLOR;
} else {
text = text + " = disabled";
color = ERROR_COLOR;
}
vp.addTextElement(text, color);
for (Entry parameter : e.getValue().getParameters().entrySet()) {
vp.add10PtTextElement(parameter.getKey() + " = " + parameter.getValue(), 10);
}
}
}
private void writeEnvironment() {
Section env = report.createSection("Environment");
env.addTextElement("The checks were executed under the following environment:");
Table t = new Table(2);
t.setTitle("Name", "Value");
t.addRow("City Doctor Version", Localization.getText(Localization.VERSION));
t.addRow("Java VM-Version", System.getProperties().getProperty("java.vm.version"));
t.addRow("Java VM-Vendor", System.getProperties().getProperty("java.vm.vendor"));
t.addRow("Java Version", System.getProperties().getProperty("java.version"));
t.addRow("OS Name", System.getProperties().getProperty("os.name"));
t.addRow("OS Architecture", System.getProperties().getProperty("os.arch"));
env.addTable(t);
}
@Override
public void report(CityObject co) {
List errorList = new ArrayList<>();
co.collectContainedErrors(errorList);
Set errors = new HashSet<>(errorList);
for (CheckError e : errors) {
AtomicInteger errorCount = errorStatistics.computeIfAbsent(e.getErrorId(), k -> new AtomicInteger(0));
errorCount.incrementAndGet();
}
boolean hasError = containsError(co);
if (co instanceof Building) {
reportBuilding(co, hasError);
} else if (co instanceof Vegetation) {
reportVegetation(co, hasError);
} else if (co instanceof TransportationObject) {
reportTrans(co, hasError);
} else if (co instanceof BridgeObject) {
reportBridge(co, hasError);
} else if (co instanceof WaterObject) {
reportWater(co, hasError);
} else if (co instanceof LandObject) {
reportLand(co, hasError);
} else {
throw new IllegalStateException("Unknown City Object found: " + co.getClass());
}
}
private void reportLand(CityObject co, boolean hasError) {
if (land == null) {
land = vr.createSubSection("Land Objects");
}
Section lSection = land.createSubSection(co.getGmlId().getGmlString());
sectionMap.put(co.getGmlId().getGmlString(), lSection);
if (hasError) {
numErrorLand++;
lSection.setHeadlineColor(ERROR_COLOR);
} else {
numOkLand++;
lSection.setHeadlineColor(OK_COLOR);
}
writeErrorForCityObject(co, lSection);
}
private void reportWater(CityObject co, boolean hasError) {
if (water == null) {
water = vr.createSubSection("Water Objects");
}
Section wSection = water.createSubSection(co.getGmlId().getGmlString());
sectionMap.put(co.getGmlId().getGmlString(), wSection);
if (hasError) {
numErrorWater++;
wSection.setHeadlineColor(ERROR_COLOR);
} else {
numOkWater++;
wSection.setHeadlineColor(OK_COLOR);
}
writeErrorForCityObject(co, wSection);
}
private void reportBridge(CityObject co, boolean hasError) {
if (bridge == null) {
bridge = vr.createSubSection("Bridge Objects");
}
Section bSection = bridge.createSubSection(co.getGmlId().getGmlString());
sectionMap.put(co.getGmlId().getGmlString(), bSection);
if (hasError) {
numErrorBridge++;
bSection.setHeadlineColor(ERROR_COLOR);
} else {
numOkBridge++;
bSection.setHeadlineColor(OK_COLOR);
}
writeErrorForCityObject(co, bSection);
}
private void reportTrans(CityObject co, boolean hasError) {
if (trans == null) {
trans = vr.createSubSection("Transportation Objects");
}
Section tSection = trans.createSubSection(co.getGmlId().getGmlString());
sectionMap.put(co.getGmlId().getGmlString(), tSection);
if (hasError) {
numErrorTrans++;
tSection.setHeadlineColor(ERROR_COLOR);
} else {
numOkTrans++;
tSection.setHeadlineColor(OK_COLOR);
}
writeErrorForCityObject(co, tSection);
TransportationObject to = (TransportationObject) co;
for (TransportationObject transO : to.getChildren()) {
writeCheckResultForTransportationObject(transO, tSection);
}
}
private void reportVegetation(CityObject co, boolean hasError) {
if (vegetation == null) {
vegetation = vr.createSubSection("Vegetation");
}
Section vSection = vegetation.createSubSection(co.getGmlId().getGmlString());
sectionMap.put(co.getGmlId().getGmlString(), vSection);
if (hasError) {
numErrorVegetation++;
vSection.setHeadlineColor(ERROR_COLOR);
} else {
numOkVegetation++;
vSection.setHeadlineColor(OK_COLOR);
}
writeErrorForCityObject(co, vSection);
}
private void reportBuilding(CityObject co, boolean hasError) {
if (buildings == null) {
buildings = vr.createSubSection("Buildings");
}
Section bSection = buildings.createSubSection(co.getGmlId().getGmlString());
sectionMap.put(co.getGmlId().getGmlString(), bSection);
if (hasError) {
bSection.setHeadlineColor(ERROR_COLOR);
} else {
bSection.setHeadlineColor(OK_COLOR);
}
writeErrorForCityObject(co, bSection);
Building b = (Building) co;
for (BuildingPart bp : b.getBuildingParts()) {
sectionMap.put(bp.getGmlId().getGmlString(), bSection);
writeCheckResultForBuildingPart(bp, bSection);
}
for (BoundarySurface bs : b.getBoundarySurfaces()) {
writeCheckResultForBoundarySurface(bs, bSection);
}
}
private void writeCheckResultForTransportationObject(TransportationObject to, Section root) {
Map results = to.getAllCheckResults();
writeCheckResults(results.values(), root);
for (Geometry geom : to.getGeometries()) {
writeCheckResultForGeometry(geom, root);
}
}
private void writeCheckResultForBuildingPart(BuildingPart bp, Section root) {
Map results = bp.getAllCheckResults();
writeCheckResults(results.values(), root);
for (Geometry geom : bp.getGeometries()) {
writeCheckResultForGeometry(geom, root);
}
for (BoundarySurface bs : bp.getBoundarySurfaces()) {
writeCheckResultForBoundarySurface(bs, root);
}
}
private void writeErrorForCityObject(CityObject co, Section section) {
Map results = co.getAllCheckResults();
writeCheckResults(results.values(), section);
for (Geometry geom : co.getGeometries()) {
writeCheckResultForGeometry(geom, section);
}
}
private void writeCheckResultForBoundarySurface(BoundarySurface bs, Section root) {
Map results = bs.getAllCheckResults();
writeCheckResults(results.values(), root);
for (Opening o : bs.getOpenings()) {
writeCheckResultForOpening(o, root);
}
}
private void writeCheckResultForOpening(Opening o, Section root) {
Map results = o.getAllCheckResults();
writeCheckResults(results.values(), root);
}
private void writeCheckResultForGeometry(Geometry geom, Section root) {
Map results = geom.getAllCheckResults();
writeCheckResults(results.values(), root);
for (Polygon poly : geom.getPolygons()) {
writeCheckResultForPolygon(poly, root);
}
}
private void writeCheckResultForPolygon(Polygon poly, Section root) {
Map results = poly.getAllCheckResults();
writeCheckResults(results.values(), root);
writeCheckResultForLinearRing(poly.getExteriorRing(), root);
for (LinearRing ring : poly.getInnerRings()) {
writeCheckResultForLinearRing(ring, root);
}
}
private void writeCheckResultForLinearRing(LinearRing ring, Section root) {
Map results = ring.getAllCheckResults();
writeCheckResults(results.values(), root);
}
private void writeCheckResults(Collection results, Section root) {
for (CheckResult cr : results) {
if (cr.getResultStatus() == ResultStatus.ERROR) {
writeError(root, cr.getError());
}
}
}
private void writeError(Section root, CheckError error) {
String text = "Error " + error.getErrorId().toString() + ":";
root.add12PtTextElement(text, 0);
PdfErrorHandler handler = new PdfErrorHandler(root, config.getParserConfiguration());
error.report(handler);
root.addError(error);
}
private boolean containsError(CityObject co) {
return co.containsAnyError();
}
@Override
public void finishReport() throws CheckReportWriteException {
for (Section s : buildings.getSubSections()) {
if (!s.hasErrors()) {
numOkBuildings++;
// building has no errors, no table
continue;
}
numErrorBuildings++;
Table t = new Table(2);
t.setTableColumnWidth(75, 25);
t.setTitle("Error", "Count");
for (Entry e : s.getStats().getErrorCounts().entrySet()) {
t.addRow(e.getKey(), e.getValue().toString());
}
s.addTable(1, t);
}
int numBuildings = numErrorBuildings + numOkBuildings;
if (numBuildings > 0) {
statistics.addDistributionBar("Buildings", numErrorBuildings, numOkBuildings);
}
int numVegetation = numErrorVegetation + numOkVegetation;
if (numVegetation > 0) {
statistics.addDistributionBar("Vegetation", numErrorVegetation, numOkVegetation);
}
int numtr = numErrorTrans + numOkTrans;
if (numtr > 0) {
statistics.addDistributionBar("Transportation Objects", numErrorTrans, numOkTrans);
}
int numbri = numErrorBridge + numOkBridge;
if (numbri > 0) {
statistics.addDistributionBar("Bridge Objects", numErrorBridge, numOkBridge);
}
int numWater = numErrorWater + numOkWater;
if (numWater > 0) {
statistics.addDistributionBar("Water Objects", numErrorWater, numOkWater);
}
int numLand = numErrorLand + numOkLand;
if (numLand > 0) {
statistics.addDistributionBar("Land Objects", numErrorLand, numOkLand);
}
statistics.addTextElement("Error Statistics:");
for (Entry e : errorStatistics.entrySet()) {
statistics.add10PtTextElement(e.getKey().toString() + ": " + e.getValue().intValue(), 10);
}
report.save(outFile);
}
@Override
public void reportGlobalError(CheckError err) {
AtomicInteger errorCount = errorStatistics.computeIfAbsent(err.getErrorId(), k -> new AtomicInteger(0));
errorCount.incrementAndGet();
if (globalErrors == null) {
globalErrors = vr.createSubSection("Global Errors");
}
String text = "Global error " + err.getErrorId();
globalErrors.add10PtTextElement(text, 10);
PdfErrorHandler handler = new PdfErrorHandler(globalErrors, config.getParserConfiguration());
err.report(handler);
}
@Override
public void addError(String gmlId, CheckError err) {
AtomicInteger errorCount = errorStatistics.computeIfAbsent(err.getErrorId(), k -> new AtomicInteger(0));
errorCount.incrementAndGet();
Section section = sectionMap.get(gmlId);
if (section == null) {
logger.warn("No section found for gml id: {}, adding to global errors instead", gmlId);
reportGlobalError(err);
} else {
section.setHeadlineColor(ERROR_COLOR);
writeError(section, err);
section.addError(err);
}
}
}