C++ Boost graph libary Add_vertex with custom properties and check for redundancy

Viewed 63

I want to create a boost graph from a GeoJSON file containg a network of linestrings. Some linestrings have common nodes. In other words: everything is somehow connected. the file looks like this: (here only 3 linestrings: in reality more than 8000).

{"type": "Feature", "properties": {"id": 1}, "geometry": { "type": "LineString", "coordinates": [ [ 147.0, -4.8 ], [ 141.0, -2.0 ] ]}},
{"type": "Feature", "properties": {"id": 2}, "geometry": { "type": "LineString", "coordinates": [ [ 152.6, -5.2 ], [ 152.05, -3.8 ], [ 147.0, -4.8 ] ] } },
{"type": "Feature", "properties": {"id": 3}, "geometry": { "type": "LineString", "coordinates": [ [ 147.0, -4.8 ], [ 144.73, 0.0 ] ] } },

You see that the coordinate [147.0, -4.8] is part of all 3 linestrings.

I iterate over this file and save these information in a vector linestrings containing struct variables called linestring:

struct linestring //data of one linestring
{
    int id;
    std::vector<std::vector<double>> pos; //2D vector conatining x and y positions
};
std::vector <linestring> linestrings; //containing the inforamtion of all linestrings

Now I want to use this to build a boost graph:

struct Nodes
{
    double x;
    double y;
};
struct Legs
{
    int id;
    double distance; 
    //edge weight calculated with distance=sqrt((x2-x1)^2+(y2-y1)^2) euclidean distance

};

typedef adjacency_list<listS, vecS, undirectedS, Nodes, Legs> graph_t;
graph_t LinestringGraph;

Which commands are recommended for such a job?

I don't want to add all vertexes and check for redundancy by iterating or other time consuming stuff.

Is there a possibility to add an edge with the custom id and the calculated edge weight together with the vertexes containing the custom property of xy-coordinate.

I want to do something like this: PSEUDOCODE:

iterate over the vector linestrings
count the number of waypoints in the linestring
//e.g. in id=2 are "coordinates": 
// [ [ 152.6, -5.2 ], [ 152.05, -3.8 ], [ 147.0, -4.8 ] ]
//edge from [ 152.6, -5.2 ] to [ 152.05, -3.8 ] 
// and from  [ 152.05, -3.8 ] to [ 147.0, -4.8 ]
add counted edges to the boost graph 
   and weight with euclidean distance of start and end coordinate of 
   the current (part)linestring.
if one vertex already exists (e.g. [ 147.0, -4.8 ]) 
    do not create a new vertex --> use the existing

###################################### AFTER the very helpful answer of @sehe and @ravenspoint (Thank you for the very quick reply. I never expect such help) I tried this:

#include <iostream>
#include <vector>
#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/graph_utility.hpp>

using namespace boost;

struct point  // single point/node
{
    double lon, lat; //not yet x, y because I want to use longitude and latitude
    auto operator==(const point &other) 
    {
        return abs(this->lon - other.lon)< 0.000000000001 
               && abs(this->lat - other.lat < 0.000000000001;
    } 
//thanks for the operator hint from @sehe
};

struct Legs
{
    int id;
    double Distance;  // edge weight
};

struct linestring  // data of single linestring
{
    int id;
    std::vector<point> coords;  // vector with double lon/lat
};

using G = boost::adjacency_list<boost::vecS, boost::vecS, boost::undirectedS,
                                point, Legs>;
using V = G::vertex_descriptor;
using E = G::edge_descriptor;

int main()
{
    std::vector<linestring> linestrings;  // vector with all linestrings
    //some not relevant code for this question: reading in the JSON file with poco
    //everything is now stored in the vector linestrings
    //you can acess it in the following way:
    for (linestring i : linestrings)
    {
        std::cout << std::endl;
        std::cout << "id: " << i.id << std::endl;
        std::cout << "Number of waypoints: " 
                  << i.coords.size() << " \nwaypoints are: (lon, lat): ";
        for (point waypoint : i.coords)
        {
            std::cout << std::endl;
            std::cout << "(" << waypoint.lon << ", " << waypoint.lat << ")";
        }
        std::cout << std::endl;
    }
    std::map<point, V> mapping;
    for (auto &f : linestrings)
    {
        for (auto &p : f.coords)
        {
            std::cout << std::endl;
            std::cout << p.lon << ", " << p.lat << std::endl;
            if (auto it = mapping.find(p); it == mapping.end())
            {
                mapping.emplace(p, add_vertex(p, g));
            }
        }
    };
    return 0;
}

With this I get the error: error C2678: binary '<': no operator found which takes a left-hand operand of type 'const _Ty' (or there is no acceptable conversion) I tried instead of operator== the operator< but still same result/error

after the line from @sehe

int next_edge_id = 0;

I don't understand how to proceed and add the edges to the graph according to a linestring in the linestrings vector.

2 Answers

I don't want to add all vertexes and check for redundancy by iterating or other time consuming stuff.

Iterating is how computers accomplish stuff. The STL containers help you do it fast and efficiently, with very few lines of code. Like this:

void cVertex::indexer(std::vector<cVertex>& vertexVector)
{
    auto it = std::find(
        vertexVector.begin(),
        vertexVector.end(),
        *this);
    if (it == vertexVector.end())
    {
        // new unique vertex
        myIndex = vertexVector.size();
        vertexVector.push_back(*this);
    }
    else
    {
        // duplicate vertex
        myIndex = it->myIndex;
    }
}

void cSolution::indexUniqueVertices()
{
     std::vector<cVertex> vertexVector;

    // loop over all line strings
    for (auto &l : myLineString)
    {
        // loop over edges in line string
        for (auto &e : l.myEdge)
        {
            // index the vertices
            e.v1.indexer(vertexVector);
            e.v2.indexer(vertexVector);
        }
    }

Output is

unique vertices
0 (147 -4.8)
1 (141 -2)
2 (152.6 -5.2)
3 (152.05 -3.8)
4 (144.73 0)

{"type": "Feature", "properties": {"id": 1}, "geometry": { "type": "LineString", 
"coordinates": [ [ 147.0, -4.8 ], [ 141.0, -2.0 ] ]}}
has edges:
0 (147 -4.8) to 1 (141 -2)

{"type": "Feature", "properties": {"id": 2}, "geometry": { "type": "LineString", 
"coordinates": [ [ 152.6, -5.2 ], [ 152.05, -3.8 ], [ 147.0, -4.8 ] ] } }
has edges:
2 (152.6 -5.2) to 3 (152.05 -3.8)
3 (152.05 -3.8) to 0 (147 -4.8)

{"type": "Feature", "properties": {"id": 3}, "geometry": { "type": "LineString", 
"coordinates": [ [ 147.0, -4.8 ], [ 144.73, 0.0 ] ] } }
has edges:
0 (147 -4.8) to 4 (144.73 0)

Code for the complete application at https://github.com/JamesBremner/GeoJSONtoGraph/blob/main/src/main.cpp

PERFORMANCE

This code can handle 10,000 vertices in 0.1 second.

Most of the time is spent searching through the vector checking for a redundant node. Here is the detailed timing profile

Calls           Mean (secs)     Total           Scope
      1        0.098734        0.098734        vector
  20008        4.79804e-06     0.0959992       vector find

I thought that using a set, which uses a binary search, might be faster at the cost of some extra code complexity.

Calls           Mean (secs)     Total           Scope
       1        3.19261         3.19261         set
   20008        0.00015777      3.15666         set find

Surprising!

To make @ravenspoint's suggestion concrete, I'd flip it around a bit:

  • LOOP over waypoints, creating vertices for each unseen coordinate, storing a mapping point→vertex_descriptor.

  • LOOP over linestrings, looking up the previously mapped vertices

Live On Coliru

#include <boost/json/src.hpp>
#include <iostream>
namespace json = boost::json;
using json::value_from;

using coord = double;

struct point {
    coord x, y;

    auto operator<=>(point const&) const = default;

    friend std::ostream& operator<<(std::ostream& os, point const& p) {
        return os << "(" << p.x << "," << p.y << ")";
    }

    friend void tag_invoke(json::value_from_tag, json::value& jv, point const& p) {
        jv = {value_from(p.x), value_from(p.y)};
    }
    friend point tag_invoke(json::value_to_tag<point>, json::value const& jv) {
        auto& arr = jv.as_array();
        return {arr[0].as_double(), arr[1].as_double()};
    }
};

using linestring = std::vector<point>;
struct feature {
    intmax_t   id;
    linestring geom;

    friend void tag_invoke(json::value_from_tag, json::value& jv, feature const& f) {
        jv = {{"type", "Feature"},
              {"properties", {"id", f.id}},
              {
                  "geometry",
                  {
                      {"type", "LineString"},
                      {"coordinates", value_from(f.geom)},
                  },
              }};
    }
    friend feature tag_invoke(json::value_to_tag<feature>, json::value const& jv) {
        assert(jv.at("type") == "Feature");
        auto& props = jv.at("properties").as_object();
        auto& geom  = jv.at("geometry").as_object();
        assert(geom.at("type") == "LineString");

        return {
            props.at("id").as_int64(),
            json::value_to<linestring>(geom.at("coordinates")),
        };
    }
};
using features = std::vector<feature>;

#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/graph_utility.hpp>
struct Legs {
    int    id;
    double distance;
};

using G = boost::adjacency_list<boost::vecS, boost::vecS, boost::undirectedS,
                                point, Legs>;
using V = G::vertex_descriptor;
using E = G::edge_descriptor;

int main() {
    auto ff = value_to<features>(json::parse(R"([
        {"type": "Feature", "properties": {"id": 1}, "geometry": { "type": "LineString", "coordinates": [ [ 147.0, -4.8 ], [ 141.0, -2.0 ] ]}},
        {"type": "Feature", "properties": {"id": 2}, "geometry": { "type": "LineString", "coordinates": [ [ 152.6, -5.2 ], [ 152.05, -3.8 ], [ 147.0, -4.8 ] ] } },
        {"type": "Feature", "properties": {"id": 3}, "geometry": { "type": "LineString", "coordinates": [ [ 147.0, -4.8 ], [ 144.73, 0.0 ] ] } }
    ])"));

    G                  g;
    std::map<point, V> mapping;

    // create a mapping for all vertices
    for (auto& f : ff)
        for (auto& p : f.geom)
            if (auto it = mapping.find(p); it == mapping.end())
                mapping.emplace(p, add_vertex(p, g));

    int next_edge_id = 0;

    for (auto& f : ff) {
        auto waypoints = boost::make_iterator_range(f.geom);
        if (waypoints.empty())
            continue;

        V s = mapping.at(waypoints.front()), t;

        for (; waypoints.pop_front(), !waypoints.empty(); s = t) {
            t = mapping.at(waypoints.front());

            auto dx = g[t].x- g[s].x;
            auto dy = g[t].y- g[s].y;

            add_edge(s, t, Legs{next_edge_id++, sqrt(dx * dx + dy * dy)}, g);
        }
    }

    print_graph(g, get(boost::vertex_bundle, g));
}

Prints

(147,-4.8) <--> (141,-2) (152.05,-3.8) (144.73,0) 
(141,-2) <--> (147,-4.8) 
(152.6,-5.2) <--> (152.05,-3.8) 
(152.05,-3.8) <--> (152.6,-5.2) (147,-4.8) 
(144.73,0) <--> (147,-4.8) 

CAVEAT

Keep in mind floating point inexactness. You might want to implement operator< differently for point so that Very Close Points(TM) may be treated as identical.

Related