Raw File
VerticalSliceFromLayers.cpp
/**
 * \file
 * \copyright
 * Copyright (c) 2012-2021, OpenGeoSys Community (http://www.opengeosys.org)
 *            Distributed under a Modified BSD License.
 *              See accompanying file LICENSE.txt or
 *              http://www.opengeosys.org/project/license
 */

#include <algorithm>
#include <cmath>
#include <memory>
#include <string>
#include <vector>

// ThirdParty
#include <tclap/CmdLine.h>

#include <QCoreApplication>

#include "Applications/FileIO/Gmsh/GMSHInterface.h"
#include "Applications/FileIO/Gmsh/GmshReader.h"
#include "BaseLib/FileTools.h"
#include "BaseLib/IO/readStringListFromFile.h"
#include "GeoLib/AABB.h"
#include "GeoLib/AnalyticalGeometry.h"
#include "GeoLib/GEOObjects.h"
#include "GeoLib/IO/XmlIO/Qt/XmlGmlInterface.h"
#include "GeoLib/Point.h"
#include "GeoLib/Polygon.h"
#include "GeoLib/Polyline.h"
#include "InfoLib/GitInfo.h"
#include "MathLib/MathTools.h"
#include "MathLib/Point3d.h"
#include "MeshGeoToolsLib/GeoMapper.h"
#include "MeshLib/Elements/Element.h"
#include "MeshLib/IO/VtkIO/VtuInterface.h"
#include "MeshLib/IO/readMeshFromFile.h"
#include "MeshLib/Mesh.h"
#include "MeshLib/MeshEditing/MeshRevision.h"
#include "MeshLib/MeshEditing/RemoveMeshComponents.h"
#include "MeshLib/MeshSearch/ElementSearch.h"
#include "MeshLib/MeshSurfaceExtraction.h"
#include "MeshLib/Node.h"

/// creates a vector of sampling points based on the specified resolution
std::unique_ptr<std::vector<GeoLib::Point*>> createPoints(
    MathLib::Point3d const& start,
    MathLib::Point3d const& end,
    std::size_t const n_intervals)
{
    auto points = std::make_unique<std::vector<GeoLib::Point*>>();
    points->push_back(new GeoLib::Point(start, 0));
    double const length = std::sqrt(MathLib::sqrDist(start, end));
    double const interval = length / n_intervals;

    GeoLib::Point const vec((end[0] - start[0]), (end[1] - start[1]),
                            (end[2] - start[2]));
    for (std::size_t i = 1; i < n_intervals; ++i)
    {
        points->push_back(new GeoLib::Point(
            start[0] + ((i * interval) / length * vec[0]),
            start[1] + ((i * interval) / length * vec[1]), 0, i));
    }
    points->push_back(new GeoLib::Point(end, n_intervals));
    return points;
}

/// creates a polyline to be mapped on a mesh layer
GeoLib::Polyline* createPolyline(std::vector<GeoLib::Point*> const& points)
{
    GeoLib::Polyline* line = new GeoLib::Polyline(points);
    std::size_t const length = points.size();
    for (std::size_t i = 0; i < length; ++i)
    {
        line->addPoint(i);
    }
    return line;
}

/// creates a mapped line for each of the mesh layers
std::vector<std::string> createGeometries(
    GeoLib::GEOObjects& geo,
    std::vector<std::string> const& layer_names,
    MathLib::Point3d const& pnt_start,
    MathLib::Point3d const& pnt_end,
    double const resolution)
{
    std::vector<std::string> geo_name_list;
    std::size_t const n_layers = layer_names.size();
    for (std::size_t i = 0; i < n_layers; ++i)
    {
        std::unique_ptr<MeshLib::Mesh> const layer(
            MeshLib::IO::readMeshFromFile(layer_names[i]));
        if (layer == nullptr)
        {
            ERR("Could not read file {:s}. Skipping layer...", layer_names[i]);
            continue;
        }
        if (layer->getDimension() != 2)
        {
            ERR("Layer {:d} is not a 2D mesh. Skipping layer...", i);
            continue;
        }

        std::string geo_name(std::to_string(i));
        std::unique_ptr<std::vector<GeoLib::Point*>> points(
            createPoints(pnt_start, pnt_end, resolution));
        geo.addPointVec(std::move(points), geo_name);

        auto lines = std::make_unique<std::vector<GeoLib::Polyline*>>();
        lines->push_back(createPolyline(*geo.getPointVec(geo_name)));
        geo.addPolylineVec(std::move(lines), geo_name);

        MeshGeoToolsLib::GeoMapper mapper(geo, geo_name);
        mapper.mapOnMesh(layer.get());
        geo_name_list.push_back(geo_name);
    }
    return geo_name_list;
}

/// Merges all layer geometries into one. Each layer is specified by one
/// polygon. This will ensure that GMSH deals with the subsequent assignment of
/// material groups automatically.
void mergeGeometries(GeoLib::GEOObjects& geo,
                     std::vector<std::string> const& geo_names,
                     std::string& merged_geo_name)
{
    auto points = std::make_unique<std::vector<GeoLib::Point*>>();
    auto lines = std::make_unique<std::vector<GeoLib::Polyline*>>();

    auto layer_pnts = *geo.getPointVec(geo_names[0]);
    std::size_t const pnts_per_line = layer_pnts.size();
    std::size_t const n_layers = geo_names.size();
    std::vector<std::size_t> last_line_idx(pnts_per_line, 0);

    for (std::size_t i = 0; i < pnts_per_line; ++i)
    {
        std::size_t const idx = pnts_per_line - i - 1;
        points->push_back(new GeoLib::Point(*layer_pnts[i], idx));
        last_line_idx[i] = idx;
    }
    for (std::size_t j = 1; j < n_layers; ++j)
    {
        GeoLib::Polyline* line = new GeoLib::Polyline(*points);
        for (std::size_t i = 0; i < pnts_per_line; ++i)
        {
            line->addPoint(last_line_idx[i]);
        }
        layer_pnts = *geo.getPointVec(geo_names[j]);
        for (std::size_t i = 0; i < pnts_per_line; ++i)
        {
            // check if for current point the lower layer boundary is actually
            // located below the upper boundary
            std::size_t idx = last_line_idx[pnts_per_line - i - 1];
            if ((*(*points)[idx])[2] > (*layer_pnts[i])[2])
            {
                idx = points->size();
                points->push_back(new GeoLib::Point(*layer_pnts[i], idx));
                last_line_idx[pnts_per_line - i - 1] = idx;
            }
            line->addPoint(idx);
        }
        // close polygon
        line->addPoint(line->getPointID(0));
        lines->push_back(line);
    }

    geo.addPointVec(std::move(points), merged_geo_name);
    geo.addPolylineVec(std::move(lines), merged_geo_name);
}

/// rotates the merged geometry into the XY-plane
std::pair<Eigen::Matrix3d, double> rotateGeometryToXY(
    std::vector<GeoLib::Point*>& points)
{
    // compute the plane normal
    auto const [plane_normal, d] =
        GeoLib::getNewellPlane(points.begin(), points.end());
    // rotate points into x-y-plane
    Eigen::Matrix3d const rotation_matrix =
        GeoLib::computeRotationMatrixToXY(plane_normal);
    GeoLib::rotatePoints(rotation_matrix, points.begin(), points.end());

    GeoLib::AABB aabb(points.begin(), points.end());
    double const z_shift =
        (aabb.getMinPoint()[2] + aabb.getMaxPoint()[2]) / 2.0;
    std::for_each(points.begin(), points.end(),
                  [z_shift](GeoLib::Point* p) { (*p)[2] -= z_shift; });
    return {rotation_matrix, z_shift};
}

/// This encapsulates a workaround:
/// For unknown reasons, the GML->GEO converter will not work correctly when
/// inputting the merged geometry directly. However, if the geometry is saved to
/// a file and immedeately loaded again, everything works fine.
void consolidateGeometry(GeoLib::GEOObjects& geo,
                         std::string const& output_name,
                         std::string& merged_geo_name,
                         bool const keep_gml_file)
{
    std::string const filename(output_name + ".gml");
    GeoLib::IO::XmlGmlInterface xml(geo);
    xml.export_name = merged_geo_name;
    BaseLib::IO::writeStringToFile(xml.writeToString(), filename);

    geo.removePolylineVec(merged_geo_name);
    geo.removePointVec(merged_geo_name);

    xml.readFile(filename);

    if (!keep_gml_file)
    {
        BaseLib::removeFile(filename);
        BaseLib::removeFile(filename + ".md5");
    }
}

/// converts geometry into GMSH format and creates mesh
MeshLib::Mesh* generateMesh(GeoLib::GEOObjects& geo,
                            std::string const& geo_name,
                            std::string const& output_name, double res)
{
    std::string const gmsh_geo_name(output_name + ".geo");
    std::vector<std::string> gmsh_geo;
    gmsh_geo.push_back(geo_name);
    FileIO::GMSH::GMSHInterface gmsh_io(
        geo, true, FileIO::GMSH::MeshDensityAlgorithm::FixedMeshDensity, res, 0,
        0, gmsh_geo, false, false);
    gmsh_io.writePhysicalGroups(true);
    if (!BaseLib::IO::writeStringToFile(gmsh_io.writeToString(), gmsh_geo_name))
    {
        ERR("Writing gmsh geo file '{:s}' failed.", gmsh_geo_name);
    }

    std::string const gmsh_mesh_name = output_name + ".msh";
    std::string gmsh_command = "gmsh -2 -algo meshadapt " + gmsh_geo_name;
    gmsh_command += " -o " + gmsh_mesh_name + " -format msh22";
    int const return_value = std::system(gmsh_command.c_str());
    if (return_value != 0)
    {
        ERR("Execution of gmsh command returned non-zero status, %d",
            return_value);
    }
    return FileIO::GMSH::readGMSHMesh(gmsh_mesh_name);
}

/// inverse rotation of the mesh, back into original position
void rotateMesh(MeshLib::Mesh& mesh, Eigen::Matrix3d const& rot_mat,
                double const z_shift)
{
    std::vector<MeshLib::Node*> const& nodes = mesh.getNodes();
    std::for_each(nodes.begin(), nodes.end(),
                  [z_shift](MeshLib::Node* n) { (*n)[2] += z_shift; });
    GeoLib::rotatePoints(rot_mat.transpose(), nodes.begin(), nodes.end());
}

/// removes line elements from mesh such that only triangles remain
MeshLib::Mesh* removeLineElements(MeshLib::Mesh const& mesh)
{
    std::vector<std::size_t> line_idx;
    std::vector<MeshLib::Element*> const& elems = mesh.getElements();
    std::for_each(elems.begin(), elems.end(), [&](auto e) {
        if (e->getGeomType() == MeshLib::MeshElemType::LINE)
        {
            line_idx.push_back(e->getID());
        }
    });
    if (line_idx.size() == mesh.getNumberOfElements())
    {
        return nullptr;
    }
    return MeshLib::removeElements(mesh, line_idx, "mesh");
}

void writeBoundary(MeshLib::Mesh const& mesh,
                   std::vector<std::size_t> const& idx_array,
                   std::string const& file_name)
{
    std::unique_ptr<MeshLib::Mesh> const boundary(
        MeshLib::removeElements(mesh, idx_array, "mesh"));
    if (boundary == nullptr)
    {
        ERR("Error extracting boundary '{:s}'", file_name);
        return;
    }
    MeshLib::IO::VtuInterface vtu(boundary.get());
    vtu.writeToFile(file_name + ".vtu");
}

void extractBoundaries(MeshLib::Mesh const& mesh,
                       std::string const& output_name,
                       MathLib::Point3d const& pnt_start)
{
    double const eps = mesh.getMinEdgeLength() / 100.0;
    std::unique_ptr<MeshLib::Mesh> boundary_mesh(
        MeshLib::BoundaryExtraction::getBoundaryElementsAsMesh(
            mesh, "bulk_node_ids", "bulk_element_ids", "bulk_face_ids"));

    auto const& elems = boundary_mesh->getElements();
    std::vector<std::size_t> left_bound_idx, right_bound_idx, top_bound_idx,
        bottom_bound_idx;
    Eigen::Vector2d const anchor(pnt_start[0], pnt_start[1]);
    for (auto e : elems)
    {
        Eigen::Vector2d const n1((*e->getNode(0))[0], (*e->getNode(0))[1]);
        Eigen::Vector2d const n2((*e->getNode(1))[0], (*e->getNode(1))[1]);
        Eigen::Vector2d const dist1(n1 - anchor);
        Eigen::Vector2d const dist2(n2 - anchor);
        std::size_t const id = e->getID();
        // elements located at left or right side
        if ((dist1 - dist2).squaredNorm() < eps)
        {
            top_bound_idx.push_back(id);
            bottom_bound_idx.push_back(id);

            if (dist1.squaredNorm() < eps)
            {
                right_bound_idx.push_back(id);
            }
            else
            {
                left_bound_idx.push_back(id);
            }
            continue;
        }
        // elements located at top or bottom
        if (dist2.squaredNorm() < dist1.squaredNorm())
        {
            top_bound_idx.push_back(id);
            left_bound_idx.push_back(id);
            right_bound_idx.push_back(id);
        }
        else
        {
            bottom_bound_idx.push_back(id);
            left_bound_idx.push_back(id);
            right_bound_idx.push_back(id);
        }
    }

    writeBoundary(*boundary_mesh, left_bound_idx, output_name + "_left");
    writeBoundary(*boundary_mesh, right_bound_idx, output_name + "_right");
    writeBoundary(*boundary_mesh, top_bound_idx, output_name + "_top");
    writeBoundary(*boundary_mesh, bottom_bound_idx, output_name + "_bottom");
}

int main(int argc, char* argv[])
{
    QCoreApplication a(argc, argv);
    TCLAP::CmdLine cmd(
        "Creates a triangle-mesh of a vertical slice out of a list of input "
        "layer meshes. The slice is defined by a start- and end-point. In "
        "addition, the resolution for meshing the extracted slice (i.e. the "
        "maximum edge length of the domain discretisation) needs to be "
        "specified. The utility requires access to the meshing utility GMSH to "
        "work correctly.\n\n"
        "OpenGeoSys-6 software, version " +
            GitInfoLib::GitInfo::ogs_version +
            ".\n"
            "Copyright (c) 2012-2021, OpenGeoSys Community "
            "(http://www.opengeosys.org)",
        ' ', GitInfoLib::GitInfo::ogs_version);
    TCLAP::SwitchArg test_arg("t", "testdata", "keep test data", false);
    cmd.add(test_arg);
    TCLAP::SwitchArg bound_arg("b", "bounds", "save mesh boundaries", false);
    cmd.add(bound_arg);
    TCLAP::ValueArg<double> res_arg(
        "r", "resolution",
        "desired edge length of triangles in the resulting slice.", true, 0,
        "floating point number");
    cmd.add(res_arg);
    TCLAP::ValueArg<double> end_y_arg(
        "", "end-y", "y-coordinates of the end point defining the slice", true,
        0, "floating point number");
    cmd.add(end_y_arg);
    TCLAP::ValueArg<double> end_x_arg(
        "", "end-x", "x-coordinates of the end point defining the slice", true,
        0, "floating point number");
    cmd.add(end_x_arg);
    TCLAP::ValueArg<double> start_y_arg(
        "", "start-y", "y-coordinates of the start point defining the slice",
        true, 0, "floating point number");
    cmd.add(start_y_arg);
    TCLAP::ValueArg<double> start_x_arg(
        "", "start-x", "x-coordinates of the start point defining the slice",
        true, 0, "floating point number");
    cmd.add(start_x_arg);
    TCLAP::ValueArg<std::string> output_arg(
        "o", "output", "name of output mesh (*.vtu)", true, "", "string");
    cmd.add(output_arg);
    TCLAP::ValueArg<std::string> input_arg(
        "i", "input",
        "name of the input file list containing the paths the all input layers "
        "in correct order from top to bottom",
        true, "", "string");
    cmd.add(input_arg);
    cmd.parse(argc, argv);

    std::string const input_name = input_arg.getValue();
    std::string const output_name = output_arg.getValue();

    MathLib::Point3d const pnt_start{
        {start_x_arg.getValue(), start_y_arg.getValue(), 0.0}};
    MathLib::Point3d const pnt_end{
        {end_x_arg.getValue(), end_y_arg.getValue(), 0.0}};
    double const length = std::sqrt(MathLib::sqrDist(pnt_start, pnt_end));

    std::size_t const res = std::ceil(length / res_arg.getValue());
    double const interval_length = length / res;

    std::vector<std::string> const layer_names =
        BaseLib::IO::readStringListFromFile(input_name);
    if (layer_names.size() < 2)
    {
        ERR("At least two layers are required to extract a slice.");
        return EXIT_FAILURE;
    }

    GeoLib::GEOObjects geo;
    std::vector<std::string> const geo_name_list =
        createGeometries(geo, layer_names, pnt_start, pnt_end, res);

    if (geo_name_list.size() < 2)
    {
        ERR("Less than two geometries could be created from layers. Aborting "
            "extraction...");
        return EXIT_FAILURE;
    }

    std::string merged_geo_name = "merged_geometries";
    mergeGeometries(geo, geo_name_list, merged_geo_name);
    std::vector<GeoLib::Point*> points = *geo.getPointVec(merged_geo_name);
    auto const [rot_mat, z_shift] = rotateGeometryToXY(points);
    consolidateGeometry(geo, output_name, merged_geo_name, test_arg.getValue());

    std::unique_ptr<MeshLib::Mesh> mesh(
        generateMesh(geo, merged_geo_name, output_name, interval_length));
    if (!test_arg.getValue())
    {
        BaseLib::removeFile(output_name + ".geo");
        BaseLib::removeFile(output_name + ".msh");
    }
    if (mesh == nullptr)
    {
        ERR("Error generating mesh... (GMSH was unable to output mesh)");
        return EXIT_FAILURE;
    }
    rotateMesh(*mesh, rot_mat, z_shift);
    std::unique_ptr<MeshLib::Mesh> new_mesh(removeLineElements(*mesh));
    if (new_mesh == nullptr)
    {
        ERR("Error generating mesh... (GMSH created line mesh)");
        return EXIT_FAILURE;
    }

    // collapse all nodes that might have been created due to gmsh physically
    // separating layers
    MeshLib::MeshRevision rev(*new_mesh);
    std::unique_ptr<MeshLib::Mesh> revised_mesh(
        rev.simplifyMesh("RevisedMesh", new_mesh->getMinEdgeLength() / 100.0));

    if (bound_arg.getValue())
    {
        extractBoundaries(*revised_mesh, output_name, pnt_start);
    }
    MeshLib::IO::VtuInterface vtu(revised_mesh.get());
    vtu.writeToFile(output_name + ".vtu");
    return EXIT_SUCCESS;
}
back to top