From 3f5f7b8ed80fe870a14e8e21c5697a3bba05a2e6 Mon Sep 17 00:00:00 2001 From: sakirr05 Date: Sat, 22 Aug 2026 05:37:28 +0530 Subject: [PATCH 1/6] [planarFaces/sql] Adding SQL code for pgr_planarFaces --- sql/planar/CMakeLists.txt | 2 ++ sql/planar/_planarFaces.sql | 43 +++++++++++++++++++++++++++++ sql/planar/planarFaces.sql | 54 +++++++++++++++++++++++++++++++++++++ sql/sigs/pgrouting--4.1.sig | 2 ++ 4 files changed, 101 insertions(+) create mode 100644 sql/planar/_planarFaces.sql create mode 100644 sql/planar/planarFaces.sql diff --git a/sql/planar/CMakeLists.txt b/sql/planar/CMakeLists.txt index 44a7574d299..c7ddb365f45 100644 --- a/sql/planar/CMakeLists.txt +++ b/sql/planar/CMakeLists.txt @@ -5,6 +5,8 @@ set(LOCAL_FILES _isPlanar.sql isPlanar.sql + _planarFaces.sql + planarFaces.sql ) foreach (f ${LOCAL_FILES}) diff --git a/sql/planar/_planarFaces.sql b/sql/planar/_planarFaces.sql new file mode 100644 index 00000000000..0f140f7f415 --- /dev/null +++ b/sql/planar/_planarFaces.sql @@ -0,0 +1,43 @@ +/*PGR-GNU***************************************************************** +File: _planarFaces.sql + +Copyright (c) 2015-2026 pgRouting developers +Mail: project@pgrouting.org + +Copyright (c) 2026 Sakir Ahmed +Mail: sakirahmed75531 at gmail.com + +------ + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + ********************************************************************PGR-GNU*/ + +--v4.1 +CREATE FUNCTION _pgr_planarFaces( + TEXT, -- edges_sql (required) + + OUT seq BIGINT, + OUT face_id BIGINT, + OUT edge_id BIGINT, + OUT side INTEGER) + +RETURNS SETOF RECORD AS +'MODULE_PATHNAME' +LANGUAGE C VOLATILE STRICT +COST ${COST_HIGH} ROWS ${ROWS_HIGH}; + +COMMENT ON FUNCTION _pgr_planarFaces(TEXT) +IS 'pgRouting internal function'; diff --git a/sql/planar/planarFaces.sql b/sql/planar/planarFaces.sql new file mode 100644 index 00000000000..41f4bf84b67 --- /dev/null +++ b/sql/planar/planarFaces.sql @@ -0,0 +1,54 @@ +/*PGR-GNU***************************************************************** +File: planarFaces.sql + +Copyright (c) 2007-2026 pgRouting developers +Mail: project@pgrouting.org + +Copyright (c) 2026 Sakir Ahmed +Mail: sakirahmed75531 at gmail.com + +------ + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + ********************************************************************PGR-GNU*/ + +--v4.1 +CREATE FUNCTION pgr_planarFaces( + TEXT, -- edges_sql (required) + + OUT seq BIGINT, + OUT face_id BIGINT, + OUT edge_id BIGINT, + OUT side INTEGER) + +RETURNS SETOF RECORD AS +$BODY$ + SELECT seq, face_id, edge_id, side + FROM _pgr_planarFaces(_pgr_get_statement($1)) AS a; +$BODY$ +LANGUAGE SQL VOLATILE STRICT +COST ${COST_HIGH} ROWS ${ROWS_HIGH}; + + +COMMENT ON FUNCTION pgr_planarFaces(TEXT) +IS 'pgr_planarFaces +- EXPERIMENTAL +- Undirected graph +- Parameters: + - edges SQL with columns: id, source, target, cost [,reverse_cost] +- Documentation: + - ${PROJECT_DOC_LINK}/pgr_planarFaces.html +'; diff --git a/sql/sigs/pgrouting--4.1.sig b/sql/sigs/pgrouting--4.1.sig index 2e8a3eb33bf..21e3028c90b 100644 --- a/sql/sigs/pgrouting--4.1.sig +++ b/sql/sigs/pgrouting--4.1.sig @@ -220,6 +220,8 @@ _pgr_pickdelivereuclidean(text,text,double precision,integer,integer) pgr_pickdelivereuclidean(text,text,double precision,integer,integer) _pgr_pickdeliver(text,text,text,double precision,integer,integer) pgr_pickdeliver(text,text,text,double precision,integer,integer) +_pgr_planarfaces(text) +pgr_planarfaces(text) pgr_primbfs(text,anyarray,bigint) pgr_primbfs(text,bigint,bigint) pgr_primdd(text,anyarray,double precision) From 3591ae8e0720c403f61d1386440afe3c57b806a2 Mon Sep 17 00:00:00 2001 From: sakirr05 Date: Sat, 22 Aug 2026 05:45:09 +0530 Subject: [PATCH 2/6] [planarFaces/C/C++] Adding C/C++ code for pgr_planarFaces --- include/c_common/enums.h | 4 +- include/drivers/planar_driver.hpp | 55 +++++++++++ include/planar/planarFaces.hpp | 150 ++++++++++++++++++++++++++++++ include/process/planar_process.h | 59 ++++++++++++ src/cpp_common/utilities.cpp | 3 + src/planar/CMakeLists.txt | 3 + src/planar/planarFaces.c | 110 ++++++++++++++++++++++ src/planar/planar_driver.cpp | 134 ++++++++++++++++++++++++++ src/planar/planar_process.cpp | 83 +++++++++++++++++ 9 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 include/drivers/planar_driver.hpp create mode 100644 include/planar/planarFaces.hpp create mode 100644 include/process/planar_process.h create mode 100644 src/planar/planarFaces.c create mode 100644 src/planar/planar_driver.cpp create mode 100644 src/planar/planar_process.cpp diff --git a/include/c_common/enums.h b/include/c_common/enums.h index e2195070d13..d98dd5d3f5b 100644 --- a/include/c_common/enums.h +++ b/include/c_common/enums.h @@ -54,7 +54,9 @@ enum Which { // NOLINT(cppcoreguidelines-use-enum-class) EDGECOLORING, BIPARTITE, SEQUENTIAL, /* For components */ CONNECTEDCOMPONENTS, BICONNECTEDCOMPONENTS, STRONGCOMPONENTS, ARTICULATIONPOINTS, - BRIDGES, MAKECONNECTED + BRIDGES, MAKECONNECTED, + /* For planar */ + PLANARFACES }; #endif // INCLUDE_C_COMMON_ENUMS_H_ diff --git a/include/drivers/planar_driver.hpp b/include/drivers/planar_driver.hpp new file mode 100644 index 00000000000..d3af3f1c7f7 --- /dev/null +++ b/include/drivers/planar_driver.hpp @@ -0,0 +1,55 @@ +/*PGR-GNU***************************************************************** +File: planar_driver.hpp + +Copyright (c) 2026-2026 pgRouting developers +Mail: project@pgrouting.org + +Design of one process & driver file by +Copyright (c) 2025 Celia Virginia Vergara Castillo +Mail: vicky at erosion.dev + +------ + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + ********************************************************************PGR-GNU*/ + +#ifndef INCLUDE_DRIVERS_PLANAR_DRIVER_HPP_ +#define INCLUDE_DRIVERS_PLANAR_DRIVER_HPP_ +#pragma once + +#include +#include +#include +#include + +#include "c_common/enums.h" + +using IID_t_rt = struct IID_t_rt; + +namespace pgrouting { +namespace drivers { + +void do_planar( + const std::string&, + bool, + Which, + IID_t_rt*&, size_t&, + std::ostringstream&, std::ostringstream&, std::ostringstream&); + +} // namespace drivers +} // namespace pgrouting + +#endif // INCLUDE_DRIVERS_PLANAR_DRIVER_HPP_ diff --git a/include/planar/planarFaces.hpp b/include/planar/planarFaces.hpp new file mode 100644 index 00000000000..c056ee7df38 --- /dev/null +++ b/include/planar/planarFaces.hpp @@ -0,0 +1,150 @@ +/*PGR-GNU***************************************************************** +File: planarFaces.hpp + +Copyright (c) 2026-2026 pgRouting developers +Mail: project@pgrouting.org + +Copyright (c) 2026 Sakir Ahmed +Mail: sakirahmed75531 at gmail.com + +------ + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + ********************************************************************PGR-GNU*/ + +#ifndef INCLUDE_PLANAR_PLANARFACES_HPP_ +#define INCLUDE_PLANAR_PLANARFACES_HPP_ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "c_types/iid_t_rt.h" +#include "cpp_common/base_graph.hpp" +#include "cpp_common/interruption.hpp" +#include "cpp_common/messages.hpp" + +namespace pgrouting { +namespace functions { + +template +class Pgr_planarFaces : public pgrouting::Pgr_messages { + public: + typedef typename G::B_G B_G; + typedef typename boost::graph_traits::edge_descriptor E; + + /* + * results are returned on the shared IID_t_rt of the planar family: + * from_vid holds the face id, to_vid the edge id and cost the side, + * which is 1 (left) or 2 (right) + */ + struct FaceVisitor : public boost::planar_face_traversal_visitor { + const G &m_graph; + std::vector &m_results; + std::map &m_visit_count; + int64_t face_id; + int64_t seq; + + FaceVisitor(const G &graph, + std::vector &results, + std::map &visit_count) + : m_graph(graph), m_results(results), + m_visit_count(visit_count), face_id(0), seq(0) {} + + void begin_face() { ++face_id; } + + void next_edge(E e) { + int n = ++m_visit_count[e]; + IID_t_rt row; + row.from_vid = face_id; + row.to_vid = m_graph.graph[e].id; + row.cost = n; + m_results.push_back(row); + } + }; + + std::vector planarFaces(G &graph) { + CHECK_FOR_INTERRUPTS(); + + std::vector results; + + std::map e_index; + boost::associative_property_map> + e_index_map(e_index); + std::size_t edge_count = 0; + typename boost::graph_traits::edge_iterator ei, ei_end; + for (boost::tie(ei, ei_end) = boost::edges(graph.graph); + ei != ei_end; ++ei) { + boost::put(e_index_map, *ei, edge_count++); + } + + typedef std::vector vec_t; + std::vector embedding_storage(boost::num_vertices(graph.graph)); + auto embedding = boost::make_iterator_property_map( + embedding_storage.begin(), + boost::get(boost::vertex_index, graph.graph)); + + bool is_planar = false; + try { + is_planar = boost::boyer_myrvold_planarity_test( + boost::boyer_myrvold_params::graph = graph.graph, + boost::boyer_myrvold_params::embedding = embedding, + boost::boyer_myrvold_params::edge_index_map = e_index_map); + } catch (boost::exception const& ex) { + (void)ex; + throw; + } catch (std::exception &e) { + (void)e; + throw; + } catch (...) { + throw; + } + + if (!is_planar) { + throw std::string("Graph is not planar"); + } + + std::map visit_count; + FaceVisitor vis(graph, results, visit_count); + + try { + boost::planar_face_traversal( + graph.graph, embedding, vis, e_index_map); + } catch (boost::exception const& ex) { + (void)ex; + throw; + } catch (std::exception &e) { + (void)e; + throw; + } catch (...) { + throw; + } + + return results; + } +}; + +} // namespace functions +} // namespace pgrouting + +#endif // INCLUDE_PLANAR_PLANARFACES_HPP_ diff --git a/include/process/planar_process.h b/include/process/planar_process.h new file mode 100644 index 00000000000..d12151483e4 --- /dev/null +++ b/include/process/planar_process.h @@ -0,0 +1,59 @@ +/*PGR-GNU***************************************************************** +File: planar_process.h + +Copyright (c) 2026-2026 pgRouting developers +Mail: project@pgrouting.org + +Design of one process & driver file by +Copyright (c) 2025 Celia Virginia Vergara Castillo +Mail: vicky at erosion.dev + +------ + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + ********************************************************************PGR-GNU*/ + +#ifndef INCLUDE_PROCESS_PLANAR_PROCESS_H_ +#define INCLUDE_PROCESS_PLANAR_PROCESS_H_ +#pragma once + +#ifdef __cplusplus +#include +#include +using IID_t_rt = struct IID_t_rt; +#else +#include +#include +#include +typedef struct IID_t_rt IID_t_rt; +#endif + +#include "c_common/enums.h" + +#ifdef __cplusplus +extern "C" { +#endif + +void pgr_process_planar( + const char*, bool, + enum Which, + IID_t_rt**, size_t*); + +#ifdef __cplusplus +} +#endif + +#endif // INCLUDE_PROCESS_PLANAR_PROCESS_H_ diff --git a/src/cpp_common/utilities.cpp b/src/cpp_common/utilities.cpp index 71e366c8555..fdb41bc0b62 100644 --- a/src/cpp_common/utilities.cpp +++ b/src/cpp_common/utilities.cpp @@ -123,6 +123,9 @@ get_name(Which which) { case STRONGCOMPONENTS: return "pgr_strongComponents"; break; + case PLANARFACES: + return "pgr_planarFaces"; + break; default: return "unknown"; break; diff --git a/src/planar/CMakeLists.txt b/src/planar/CMakeLists.txt index a7187b065ee..3a9631616a9 100644 --- a/src/planar/CMakeLists.txt +++ b/src/planar/CMakeLists.txt @@ -4,4 +4,7 @@ ADD_LIBRARY(planar OBJECT isPlanar.c isPlanar_driver.cpp + planarFaces.c + planar_driver.cpp + planar_process.cpp ) diff --git a/src/planar/planarFaces.c b/src/planar/planarFaces.c new file mode 100644 index 00000000000..191a6127e4a --- /dev/null +++ b/src/planar/planarFaces.c @@ -0,0 +1,110 @@ +/*PGR-GNU***************************************************************** +File: planarFaces.c + +Generated with Template by: +Copyright (c) 2015-2026 pgRouting developers +Mail: project@pgrouting.org + +Function's developer: +Copyright (c) 2026 Sakir Ahmed +Mail: sakirahmed75531 at gmail.com + +------ + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + ********************************************************************PGR-GNU*/ + +#include +#include "c_common/postgres_connection.h" +#include "c_types/iid_t_rt.h" +#include "process/planar_process.h" + +PGDLLEXPORT Datum _pgr_planarfaces(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(_pgr_planarfaces); + + +PGDLLEXPORT Datum +_pgr_planarfaces(PG_FUNCTION_ARGS) { + FuncCallContext *funcctx; + TupleDesc tuple_desc; + + IID_t_rt *result_tuples = NULL; + size_t result_count = 0; + + if (SRF_IS_FIRSTCALL()) { + MemoryContext oldcontext; + funcctx = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + pgr_process_planar( + text_to_cstring(PG_GETARG_TEXT_P(0)), + false, + + PLANARFACES, + &result_tuples, + &result_count); + + funcctx->max_calls = result_count; + funcctx->user_fctx = result_tuples; + if (get_call_result_type(fcinfo, NULL, &tuple_desc) + != TYPEFUNC_COMPOSITE) { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("function returning record called in context " + "that cannot accept type record"))); + } + + funcctx->tuple_desc = tuple_desc; + MemoryContextSwitchTo(oldcontext); + } + + funcctx = SRF_PERCALL_SETUP(); + tuple_desc = funcctx->tuple_desc; + result_tuples = (IID_t_rt*) funcctx->user_fctx; + + if (funcctx->call_cntr < funcctx->max_calls) { + HeapTuple tuple; + Datum result; + Datum *values; + bool* nulls; + size_t call_cntr = funcctx->call_cntr; + + size_t numb = 4; + values = palloc(numb * sizeof(Datum)); + nulls = palloc(numb * sizeof(bool)); + + size_t i; + for (i = 0; i < numb; ++i) { + nulls[i] = false; + } + + /* + * on the shared IID_t_rt: from_vid is the face id, to_vid the edge id + * and cost the side, which is 1 (left) or 2 (right) + */ + values[0] = UInt64GetDatum(call_cntr + 1); + values[1] = Int64GetDatum(result_tuples[call_cntr].from_vid); + values[2] = Int64GetDatum(result_tuples[call_cntr].to_vid); + + values[3] = Int32GetDatum((int32_t)result_tuples[call_cntr].cost); + + tuple = heap_form_tuple(tuple_desc, values, nulls); + result = HeapTupleGetDatum(tuple); + SRF_RETURN_NEXT(funcctx, result); + } else { + SRF_RETURN_DONE(funcctx); + } +} diff --git a/src/planar/planar_driver.cpp b/src/planar/planar_driver.cpp new file mode 100644 index 00000000000..23c9c323713 --- /dev/null +++ b/src/planar/planar_driver.cpp @@ -0,0 +1,134 @@ +/*PGR-GNU***************************************************************** +File: planar_driver.cpp + +Copyright (c) 2026-2026 pgRouting developers +Mail: project@pgrouting.org + +Design of one process & driver file by +Copyright (c) 2025 Celia Virginia Vergara Castillo +Mail: vicky at erosion.dev + +------ + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + ********************************************************************PGR-GNU*/ + +#include "drivers/planar_driver.hpp" + +#include +#include +#include +#include +#include + +#include "c_types/iid_t_rt.h" +#include "cpp_common/base_graph.hpp" +#include "cpp_common/pgdata_getters.hpp" +#include "cpp_common/utilities.hpp" +#include "cpp_common/alloc.hpp" +#include "cpp_common/assert.hpp" + +#include "planar/planarFaces.hpp" + +namespace pgrouting { +namespace drivers { + +void do_planar( + const std::string &edges_sql, + bool directed, + + Which which, + + IID_t_rt* &return_tuples, + size_t &return_count, + std::ostringstream &log, + std::ostringstream ¬ice, + std::ostringstream &err) { + std::string hint = ""; + return_tuples = nullptr; + return_count = 0; + + try { + if (edges_sql.empty()) { + err << "Empty edges SQL"; + return; + } + + using pgrouting::pgget::get_edges; + using pgrouting::UndirectedGraph; + + hint = edges_sql; + auto edges = get_edges(edges_sql, true, true); + + if (edges.empty()) { + notice << "No edges found"; + log << edges_sql; + return; + } + + hint = ""; + + /* the planar family works on the undirected structure only */ + (void)directed; + + UndirectedGraph undigraph; + undigraph.insert_edges(edges); + + std::vector results; + + switch (which) { + case PLANARFACES: + { + pgrouting::functions::Pgr_planarFaces fn; + results = fn.planarFaces(undigraph); + log << fn.get_log(); + } + break; + default: + err << "planar_driver.cpp: Unknown function with name '" << get_name(which) + << "' for undirected graph"; + return; + } + + auto count = results.size(); + + if (count == 0) { + notice << "No results found"; + return; + } + + return_tuples = pgr_alloc(count, return_tuples); + for (size_t i = 0; i < count; ++i) { + return_tuples[i] = results[i]; + } + return_count = count; + } catch (AssertFailedException &except) { + err << except.what(); + } catch (const std::pair& ex) { + err << ex.first; + log << ex.second; + } catch (const std::string &ex) { + err << ex; + log << hint; + } catch (std::exception &except) { + err << except.what(); + } catch (...) { + err << "Caught unknown exception!"; + } +} + +} // namespace drivers +} // namespace pgrouting diff --git a/src/planar/planar_process.cpp b/src/planar/planar_process.cpp new file mode 100644 index 00000000000..222a9012357 --- /dev/null +++ b/src/planar/planar_process.cpp @@ -0,0 +1,83 @@ +/*PGR-GNU***************************************************************** +File: planar_process.cpp + +Copyright (c) 2026-2026 pgRouting developers +Mail: project@pgrouting.org + +Design of one process & driver file by +Copyright (c) 2025 Celia Virginia Vergara Castillo +Mail: vicky at erosion.dev + +------ + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + ********************************************************************PGR-GNU*/ + +#include "process/planar_process.h" + +#include +#include + +extern "C" { +#include "c_common/postgres_connection.h" +#include "c_common/e_report.h" +#include "c_common/time_msg.h" +} + +#include "c_types/iid_t_rt.h" + +#include "cpp_common/report_messages.hpp" +#include "cpp_common/utilities.hpp" +#include "cpp_common/assert.hpp" +#include "cpp_common/alloc.hpp" + +#include "drivers/planar_driver.hpp" + +void pgr_process_planar( + const char* edges_sql, + bool directed, + enum Which which, + IID_t_rt **result_tuples, + size_t *result_count) { + pgassert(edges_sql); + pgassert(!(*result_tuples)); + pgassert(*result_count == 0); + pgr_SPI_connect(); + + std::ostringstream log; + std::ostringstream err; + std::ostringstream notice; + + clock_t start_t = clock(); + pgrouting::drivers::do_planar( + edges_sql? edges_sql : "", + directed, + which, + (*result_tuples), (*result_count), + log, notice, err); + + auto name = std::string(" processing ") + pgrouting::get_name(which); + time_msg(name.c_str(), start_t, clock()); + + if (!err.str().empty() && (*result_tuples)) { + pfree(*result_tuples); + (*result_tuples) = nullptr; + (*result_count) = 0; + } + + pgrouting::report_messages(log, notice, err); + pgr_SPI_finish(); +} From 85b9cfd51661522b4d194d9cbe93196273cc4935 Mon Sep 17 00:00:00 2001 From: sakirr05 Date: Sat, 22 Aug 2026 05:48:12 +0530 Subject: [PATCH 3/6] [planarFaces/pgtap] Adding test files for pgr_planarFaces --- pgtap/planar/planarFaces/edge_cases.pg | 165 ++++++++++++++++++++++ pgtap/planar/planarFaces/inner_query.pg | 31 ++++ pgtap/planar/planarFaces/no_crash_test.pg | 57 ++++++++ pgtap/planar/planarFaces/types_check.pg | 38 +++++ 4 files changed, 291 insertions(+) create mode 100644 pgtap/planar/planarFaces/edge_cases.pg create mode 100644 pgtap/planar/planarFaces/inner_query.pg create mode 100644 pgtap/planar/planarFaces/no_crash_test.pg create mode 100644 pgtap/planar/planarFaces/types_check.pg diff --git a/pgtap/planar/planarFaces/edge_cases.pg b/pgtap/planar/planarFaces/edge_cases.pg new file mode 100644 index 00000000000..b9607a32daf --- /dev/null +++ b/pgtap/planar/planarFaces/edge_cases.pg @@ -0,0 +1,165 @@ +/* :file: This file is part of the pgRouting project. +:copyright: Copyright (c) 2026-2026 pgRouting developers +:license: Creative Commons Attribution-Share Alike 3.0 https://creativecommons.org/licenses/by-sa/3.0 */ + + +BEGIN; + +UPDATE edges SET cost = sign(cost), reverse_cost = sign(reverse_cost); +SELECT CASE WHEN min_version('4.1.0') THEN plan (10) ELSE plan(1) END; + +CREATE OR REPLACE FUNCTION edge_cases() +RETURNS SETOF TEXT AS +$BODY$ +BEGIN + +IF NOT min_version('4.1.0') THEN + RETURN QUERY + SELECT skip(1, 'Function is new on 4.1.0'); + RETURN; +END IF; + +-- empty graph +PREPARE q1 AS +SELECT id, source, target, cost, reverse_cost +FROM edges +WHERE id > 18; + +RETURN QUERY +SELECT is_empty('q1', '1: Graph with 0 edges'); + +PREPARE emptyTest AS +SELECT * FROM pgr_planarFaces('q1'); + +RETURN QUERY +SELECT is_empty('emptyTest', '2: Empty graph returns 0 rows'); + +-- single edge: 2 faces (each side) +PREPARE q3 AS +SELECT id, source, target, cost, reverse_cost +FROM edges +WHERE id = 1; + +PREPARE singleEdgeTest AS +SELECT * FROM pgr_planarFaces('q3'); + +RETURN QUERY +SELECT set_eq('singleEdgeTest', + $$VALUES + (1::BIGINT, 1::BIGINT, 1::BIGINT, 1), + (2::BIGINT, 1::BIGINT, 1::BIGINT, 2) + $$, + '3: Single edge has 2 face-edge incidences'); + +-- open path 11 - 7 - 8 - 12 (edges 8, 10, 12): a tree has no interior face, +-- so all 3 edges border the single exterior face, still giving 2|E| = 6 rows +PREPARE q4 AS +SELECT id, source, target, cost, reverse_cost +FROM edges +WHERE id IN (8, 10, 12); + +PREPARE triangleTest AS +SELECT count(*) FROM pgr_planarFaces('q4'); + +RETURN QUERY +SELECT set_eq('triangleTest', + $$VALUES (6::BIGINT) $$, + '4: Open path of 3 edges has 6 rows, all on the exterior face'); + +-- total rows = 2 * number of edges +PREPARE q5 AS +SELECT id, source, target, cost, reverse_cost +FROM edges; + +PREPARE fullGraphRows AS +SELECT count(*) FROM pgr_planarFaces('q5'); + +PREPARE expectedRows AS +SELECT 2 * count(*)::BIGINT FROM ( + SELECT id FROM edges + WHERE cost >= 0 OR reverse_cost >= 0 +) e; + +RETURN QUERY +SELECT set_eq('fullGraphRows', 'expectedRows', + '5: Total rows = 2 * number of edges'); + +-- each edge appears exactly twice (once per side) +PREPARE sideCheck AS +SELECT edge_id, count(*) FROM pgr_planarFaces('q5') +GROUP BY edge_id HAVING count(*) != 2; + +RETURN QUERY +SELECT is_empty('sideCheck', '6: Each edge appears exactly twice'); + +-- subgraph id < 10: 9 edges, 18 rows +PREPARE subgraphTest AS +SELECT count(*) FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges WHERE id < 10' +); + +RETURN QUERY +SELECT set_eq('subgraphTest', + $$VALUES (18::BIGINT) $$, + '7: Subgraph id < 10 has 18 rows (2 x 9 edges)'); + +-- Euler's formula on a connected planar graph: V - E + F = 2 +-- edges id < 10 form a single connected component with 9 vertices and 9 edges, +-- so the traversal must find exactly 2 faces + +PREPARE eulerConnected AS +SELECT ( + (SELECT count(*) FROM ( + SELECT source AS n FROM edges WHERE id < 10 + UNION SELECT target FROM edges WHERE id < 10) v) -- V + - (SELECT count(*) FROM edges WHERE id < 10) -- E + + (SELECT count(DISTINCT face_id) FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges WHERE id < 10')) -- F + )::BIGINT; + +RETURN QUERY +SELECT set_eq('eulerConnected', + $$VALUES (2::BIGINT) $$, + '9: Euler formula on a connected subgraph: V - E + F = 2'); + +-- Generalised Euler formula on a disconnected graph: V - E + F = 2 * C +-- the traversal walks the outer face of every component separately, so each +-- of the 3 components of the full sample network contributes 2 + +PREPARE eulerDisconnected AS +SELECT ( + (SELECT count(*) FROM ( + SELECT source AS n FROM edges UNION SELECT target FROM edges) v) + - (SELECT count(*) FROM edges) + + (SELECT count(DISTINCT face_id) FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost FROM edges')) + )::BIGINT; + +RETURN QUERY +SELECT set_eq('eulerDisconnected', + $$VALUES (6::BIGINT) $$, + '10: Euler formula on the disconnected sample network: V - E + F = 2 * 3 components'); + +-- non-planar graph (K5 minor) raises error +INSERT INTO edges (source, target, cost, reverse_cost) VALUES + (10, 16, 1, 1), (10, 13, 1, 1), + (15, 11, 1, 1), (15, 13, 1, 1), + (11, 13, 1, 1), (16, 13, 1, 1); + +RETURN QUERY +SELECT throws_ok( + $$SELECT * FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost FROM edges')$$, + 'Graph is not planar', + '8: Non-planar graph raises error'); + +END; +$BODY$ +LANGUAGE plpgsql; + +SELECT edge_cases(); + +SELECT * FROM finish(); +ROLLBACK; diff --git a/pgtap/planar/planarFaces/inner_query.pg b/pgtap/planar/planarFaces/inner_query.pg new file mode 100644 index 00000000000..3a87a78a4c8 --- /dev/null +++ b/pgtap/planar/planarFaces/inner_query.pg @@ -0,0 +1,31 @@ +/* :file: This file is part of the pgRouting project. +:copyright: Copyright (c) 2026-2026 pgRouting developers +:license: Creative Commons Attribution-Share Alike 3.0 https://creativecommons.org/licenses/by-sa/3.0 */ + + +BEGIN; + +UPDATE edges SET cost = sign(cost), reverse_cost = sign(reverse_cost); +SELECT CASE WHEN min_version('4.1.0') THEN plan (54) ELSE plan(1) END; + +CREATE OR REPLACE FUNCTION inner_query() +RETURNS SETOF TEXT AS +$BODY$ +BEGIN + +IF NOT min_version('4.1.0') THEN + RETURN QUERY + SELECT skip(1, 'Function is new on 4.1.0'); + RETURN; +END IF; + +RETURN QUERY SELECT style_dijkstra('pgr_planarfaces(', ')'); + +END; +$BODY$ +LANGUAGE plpgsql; + +SELECT inner_query(); + +SELECT finish(); +ROLLBACK; diff --git a/pgtap/planar/planarFaces/no_crash_test.pg b/pgtap/planar/planarFaces/no_crash_test.pg new file mode 100644 index 00000000000..1de50d0f018 --- /dev/null +++ b/pgtap/planar/planarFaces/no_crash_test.pg @@ -0,0 +1,57 @@ +/* :file: This file is part of the pgRouting project. +:copyright: Copyright (c) 2026-2026 pgRouting developers +:license: Creative Commons Attribution-Share Alike 3.0 https://creativecommons.org/licenses/by-sa/3.0 */ + + +BEGIN; + +UPDATE edges SET cost = sign(cost), reverse_cost = sign(reverse_cost); +SELECT CASE WHEN min_version('4.1.0') THEN plan (8) ELSE plan(1) END; + +PREPARE edges AS +SELECT id, source, target, cost, reverse_cost FROM edges; + +PREPARE null_ret AS +SELECT id FROM vertices WHERE id IN (-1); + +PREPARE null_ret_arr AS +SELECT array_agg(id) FROM vertices WHERE id IN (-1); + +CREATE OR REPLACE FUNCTION test_function() +RETURNS SETOF TEXT AS +$BODY$ +DECLARE +params TEXT[]; +subs TEXT[]; +BEGIN + IF NOT min_version('4.1.0') THEN + RETURN QUERY + SELECT skip(1, 'Function is new on 4.1.0'); + RETURN; + END IF; + + RETURN QUERY + SELECT isnt_empty('edges', 'Should not be empty true to tests be meaningful'); + RETURN QUERY + SELECT is_empty('null_ret', 'Should be empty to tests be meaningful'); + RETURN QUERY + SELECT set_eq('null_ret_arr', 'SELECT NULL::BIGINT[]', 'Should be empty to tests be meaningful'); + + params = ARRAY['$$SELECT id, source, target, cost, reverse_cost FROM edges$$']::TEXT[]; + subs = ARRAY[ + 'NULL' + ]::TEXT[]; + + RETURN QUERY + SELECT * FROM no_crash_test('pgr_planarfaces', params, subs); + RETURN QUERY + SELECT throw_on_empty_edges_sql('pgr_planarFaces', ''); + +END +$BODY$ +LANGUAGE plpgsql VOLATILE; + + +SELECT * FROM test_function(); +SELECT finish(); +ROLLBACK; diff --git a/pgtap/planar/planarFaces/types_check.pg b/pgtap/planar/planarFaces/types_check.pg new file mode 100644 index 00000000000..f5ac42fb784 --- /dev/null +++ b/pgtap/planar/planarFaces/types_check.pg @@ -0,0 +1,38 @@ +/* :file: This file is part of the pgRouting project. +:copyright: Copyright (c) 2026-2026 pgRouting developers +:license: Creative Commons Attribution-Share Alike 3.0 https://creativecommons.org/licenses/by-sa/3.0 */ + + +BEGIN; + +SELECT CASE WHEN min_version('4.1.0') THEN plan (4) ELSE plan(1) END; + +CREATE OR REPLACE FUNCTION types_check() +RETURNS SETOF TEXT AS +$BODY$ +BEGIN + +IF NOT min_version('4.1.0') THEN + RETURN QUERY + SELECT skip(1, 'Function is new on 4.1.0'); + RETURN; +END IF; + +RETURN QUERY SELECT has_function('pgr_planarfaces'); +RETURN QUERY SELECT has_function('pgr_planarfaces', ARRAY['text']); +RETURN QUERY SELECT function_returns('pgr_planarfaces', ARRAY['text'], 'setof record'); + +RETURN QUERY +SELECT function_args_eq('pgr_planarfaces', + $$VALUES + ('{"","seq","face_id","edge_id","side"}'::TEXT[]) + $$); + +END; +$BODY$ +LANGUAGE plpgsql; + +SELECT types_check(); + +SELECT * FROM finish(); +ROLLBACK; From 312c03588e16e366a85f8ba1dfe7c2a9d2a7f7de Mon Sep 17 00:00:00 2001 From: sakirr05 Date: Sat, 22 Aug 2026 05:49:12 +0530 Subject: [PATCH 4/6] [planarFaces/docqueries] Adding test documentation examples code for pgr_planarFaces --- docqueries/planar/CMakeLists.txt | 1 + docqueries/planar/planarFaces.pg | 74 +++++++++++ docqueries/planar/planarFaces.result | 182 +++++++++++++++++++++++++++ docqueries/planar/test.conf | 1 + 4 files changed, 258 insertions(+) create mode 100644 docqueries/planar/planarFaces.pg create mode 100644 docqueries/planar/planarFaces.result diff --git a/docqueries/planar/CMakeLists.txt b/docqueries/planar/CMakeLists.txt index 1995c7014e6..aeed8d3f0f4 100644 --- a/docqueries/planar/CMakeLists.txt +++ b/docqueries/planar/CMakeLists.txt @@ -5,6 +5,7 @@ set(LOCAL_FILES boyerMyrvold isPlanar + planarFaces ) foreach (f ${LOCAL_FILES}) diff --git a/docqueries/planar/planarFaces.pg b/docqueries/planar/planarFaces.pg new file mode 100644 index 00000000000..3285a6fd7ae --- /dev/null +++ b/docqueries/planar/planarFaces.pg @@ -0,0 +1,74 @@ +/* :file: This file is part of the pgRouting project. +:copyright: Copyright (c) 2026-2026 pgRouting developers +:license: Creative Commons Attribution-Share Alike 3.0 https://creativecommons.org/licenses/by-sa/3.0 */ +/* -- q1 */ +SELECT * FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges' +); +/* -- q2 */ +SELECT pgr_isPlanar( + 'SELECT id, source, target, cost, reverse_cost + FROM edges' +); +/* -- q3 */ +DROP TABLE IF EXISTS tri_edges; +CREATE TABLE tri_edges ( + id BIGINT, + source BIGINT, + target BIGINT, + cost FLOAT, + reverse_cost FLOAT +); +INSERT INTO tri_edges (id, source, target, cost, reverse_cost) VALUES +(1, 1, 2, 1, 1), +(2, 2, 3, 1, 1), +(3, 1, 3, 1, 1); +SELECT * FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost FROM tri_edges' +) ORDER BY seq; +/* -- q4 */ +SELECT * FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges WHERE id = 1' +) ORDER BY seq; +/* -- q5 */ +SELECT count(*) AS rows +FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges WHERE id IN (8, 10, 12)' +); +/* -- q6 */ +SELECT count(*) AS rows +FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges WHERE id < 10' +); +/* -- q7 */ +SELECT face_id, count(*) AS edge_count +FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges' +) +GROUP BY face_id +ORDER BY face_id; +/* -- q8 */ +SELECT * FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges WHERE id > 18' +) ORDER BY seq; +/* -- q9 */ +SELECT edge_id +FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges' +) +GROUP BY edge_id +HAVING count(*) != 2; +/* -- q10 */ +SELECT count(*) AS rows +FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges WHERE id < 10' +); +/* -- q11 */ diff --git a/docqueries/planar/planarFaces.result b/docqueries/planar/planarFaces.result new file mode 100644 index 00000000000..87ddd7bf97a --- /dev/null +++ b/docqueries/planar/planarFaces.result @@ -0,0 +1,182 @@ +BEGIN; +BEGIN +SET client_min_messages TO NOTICE; +SET +/* :file: This file is part of the pgRouting project. +:copyright: Copyright (c) 2026-2026 pgRouting developers +:license: Creative Commons Attribution-Share Alike 3.0 https://creativecommons.org/licenses/by-sa/3.0 */ +/* -- q1 */ +SELECT * FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges' +); + seq | face_id | edge_id | side +-----+---------+---------+------ + 1 | 1 | 6 | 1 + 2 | 1 | 7 | 1 + 3 | 1 | 4 | 1 + 4 | 1 | 1 | 1 + 5 | 1 | 1 | 2 + 6 | 1 | 2 | 1 + 7 | 1 | 5 | 1 + 8 | 1 | 8 | 1 + 9 | 1 | 7 | 2 + 10 | 1 | 6 | 2 + 11 | 2 | 4 | 2 + 12 | 2 | 10 | 1 + 13 | 2 | 12 | 1 + 14 | 2 | 13 | 1 + 15 | 2 | 15 | 1 + 16 | 2 | 16 | 1 + 17 | 2 | 3 | 1 + 18 | 2 | 2 | 2 + 19 | 3 | 5 | 2 + 20 | 3 | 3 | 2 + 21 | 3 | 16 | 2 + 22 | 3 | 9 | 1 + 23 | 4 | 8 | 2 + 24 | 4 | 11 | 1 + 25 | 4 | 12 | 2 + 26 | 4 | 14 | 1 + 27 | 4 | 14 | 2 + 28 | 4 | 10 | 2 + 29 | 5 | 11 | 2 + 30 | 5 | 9 | 2 + 31 | 5 | 15 | 2 + 32 | 5 | 13 | 2 + 33 | 6 | 17 | 1 + 34 | 6 | 17 | 2 + 35 | 7 | 18 | 1 + 36 | 7 | 18 | 2 +(36 rows) + +/* -- q2 */ +SELECT pgr_isPlanar( + 'SELECT id, source, target, cost, reverse_cost + FROM edges' +); + pgr_isplanar +-------------- + t +(1 row) + +/* -- q3 */ +DROP TABLE IF EXISTS tri_edges; +NOTICE: table "tri_edges" does not exist, skipping +DROP TABLE +CREATE TABLE tri_edges ( + id BIGINT, + source BIGINT, + target BIGINT, + cost FLOAT, + reverse_cost FLOAT +); +CREATE TABLE +INSERT INTO tri_edges (id, source, target, cost, reverse_cost) VALUES +(1, 1, 2, 1, 1), +(2, 2, 3, 1, 1), +(3, 1, 3, 1, 1); +INSERT 0 3 +SELECT * FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost FROM tri_edges' +) ORDER BY seq; + seq | face_id | edge_id | side +-----+---------+---------+------ + 1 | 1 | 1 | 1 + 2 | 1 | 2 | 1 + 3 | 1 | 3 | 1 + 4 | 2 | 1 | 2 + 5 | 2 | 3 | 2 + 6 | 2 | 2 | 2 +(6 rows) + +/* -- q4 */ +SELECT * FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges WHERE id = 1' +) ORDER BY seq; + seq | face_id | edge_id | side +-----+---------+---------+------ + 1 | 1 | 1 | 1 + 2 | 1 | 1 | 2 +(2 rows) + +/* -- q5 */ +SELECT count(*) AS rows +FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges WHERE id IN (8, 10, 12)' +); + rows +------ + 6 +(1 row) + +/* -- q6 */ +SELECT count(*) AS rows +FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges WHERE id < 10' +); + rows +------ + 18 +(1 row) + +/* -- q7 */ +SELECT face_id, count(*) AS edge_count +FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges' +) +GROUP BY face_id +ORDER BY face_id; + face_id | edge_count +---------+------------ + 1 | 10 + 2 | 8 + 3 | 4 + 4 | 6 + 5 | 4 + 6 | 2 + 7 | 2 +(7 rows) + +/* -- q8 */ +SELECT * FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges WHERE id > 18' +) ORDER BY seq; +NOTICE: No edges found +HINT: SELECT id, source, target, cost, reverse_cost + FROM edges WHERE id > 18 + seq | face_id | edge_id | side +-----+---------+---------+------ +(0 rows) + +/* -- q9 */ +SELECT edge_id +FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges' +) +GROUP BY edge_id +HAVING count(*) != 2; + edge_id +--------- +(0 rows) + +/* -- q10 */ +SELECT count(*) AS rows +FROM pgr_planarFaces( + 'SELECT id, source, target, cost, reverse_cost + FROM edges WHERE id < 10' +); + rows +------ + 18 +(1 row) + +/* -- q11 */ +ROLLBACK; +ROLLBACK diff --git a/docqueries/planar/test.conf b/docqueries/planar/test.conf index 3c33912e3db..68f1da07243 100644 --- a/docqueries/planar/test.conf +++ b/docqueries/planar/test.conf @@ -7,6 +7,7 @@ 'any' => { 'files' => [qw( isPlanar.pg + planarFaces.pg )] }, From 3a4924b65fb312eda35d9102122fe23daeead00b Mon Sep 17 00:00:00 2001 From: sakirr05 Date: Sat, 22 Aug 2026 05:49:28 +0530 Subject: [PATCH 5/6] [planarFaces/doc] Adding documentation for pgr_planarFaces --- doc/planar/CMakeLists.txt | 2 + doc/planar/pgr_planarFaces.rst | 466 +++++++++++++++++++++++++++++++++ doc/planar/planar-family.rst | 37 +++ doc/src/experimental.rst | 8 +- 4 files changed, 510 insertions(+), 3 deletions(-) create mode 100644 doc/planar/pgr_planarFaces.rst create mode 100644 doc/planar/planar-family.rst diff --git a/doc/planar/CMakeLists.txt b/doc/planar/CMakeLists.txt index d9b910d35ea..1458322cf9d 100644 --- a/doc/planar/CMakeLists.txt +++ b/doc/planar/CMakeLists.txt @@ -3,7 +3,9 @@ # License: GPL-2 See https://github.com/pgRouting/pgrouting/blob/main/LICENSE set(LOCAL_FILES + planar-family.rst pgr_isPlanar.rst + pgr_planarFaces.rst ) foreach (f ${LOCAL_FILES}) diff --git a/doc/planar/pgr_planarFaces.rst b/doc/planar/pgr_planarFaces.rst new file mode 100644 index 00000000000..6e1ed4e8252 --- /dev/null +++ b/doc/planar/pgr_planarFaces.rst @@ -0,0 +1,466 @@ +:file: This file is part of the pgRouting project. +:copyright: Copyright (c) 2026-2026 pgRouting developers +:license: Creative Commons Attribution-Share Alike 3.0 https://creativecommons.org/licenses/by-sa/3.0 + +.. index:: + single: Planar Family ; pgr_planarFaces - Experimental + single: planarFaces - Experimental on v4.1 + +| + +``pgr_planarFaces`` - Experimental +=============================================================================== + +``pgr_planarFaces`` - Identifies the faces of a planar embedding and lists every +edge-face incidence for an undirected graph. + +.. include:: experimental.rst + :start-after: warning-begin + :end-before: end-warning + +.. rubric:: Availability + +.. rubric:: Version 4.1.0 + +* New experimental function. + +Description +------------------------------------------------------------------------------- + +In a **planar embedding**, the graph is drawn in the plane so that edges meet only +at vertices. A **face** is a maximal connected region of the plane bounded by +edges: on a street network, the city blocks between the roads. Every bounded face +is a cycle of edges enclosing an interior region. The **exterior face**, also +called the unbounded face, is the region outside the outermost cycle. + +Once an embedding is fixed, each undirected edge has a **left** and a **right** +side when walked in the direction the embedding assigns around each vertex. +In a connected planar graph, each edge separates exactly two faces, so it +contributes one row on the left and one on the right. ``pgr_planarFaces`` returns +those incidences: ``face_id`` names the face, ``edge_id`` is the border edge, and +``side`` is ``1`` (left) or ``2`` (right) relative to the embedding. + +The function first computes a planar embedding with the Boyer-Myrvold planarity +test, then walks faces with Boost ``planar_face_traversal``. Only **undirected** +structure matters: ``cost`` and ``reverse_cost`` are ignored. The input must be +**planar**; otherwise the call raises ``ERROR: Graph is not planar``. When +planarity is unknown, run :doc:`pgr_isPlanar` first. The result has one row per +edge-side pair, so a graph with :math:`|E|` edges yields :math:`2|E|` rows when +extraction succeeds. Face identifiers come from the traversal; one is the +exterior face. An empty edge SQL emits a notice and returns no rows. Running +time is :math:`O(|V| + |E|)`. + +The number of faces obeys **Euler's formula**. On a connected planar graph +:math:`|V| - |E| + |F| = 2`. The traversal walks the outer face of every +connected component separately, so on a graph with :math:`C` components the +relation generalises to :math:`|V| - |E| + |F| = 2C`. On the +:doc:`sampledata` network, which has :math:`3` components, +:math:`17 - 18 + 7 = 6 = 2 \times 3`. + +|Boost| Boost Graph Inside + +.. rubric:: References + +* Boyer, J. M. and Myrvold, W. J. (2004). On the Cutting Edge: Simplified O(n) + Planarity Algorithms by Edge Addition. Journal of Graph Algorithms and + Applications, 8(3), 241-273. + +* Boost Graph Library: `Planar Face Traversal + `__ + +Signatures +------------------------------------------------------------------------------- + +.. rubric:: Summary + +.. admonition:: \ \ + :class: signatures + + | pgr_planarFaces(`Edges SQL`_) + + | Returns set of ``(seq, face_id, edge_id, side)`` + +:Example: Faces of the full :doc:`sampledata` graph + +.. literalinclude:: planarFaces.queries + :start-after: -- q1 + :end-before: -- q2 + +.. rubric:: Explanation + +* The query returns **36 rows**, twice the **18** edges in ``edges``, consistent + with :math:`2|E|`. +* ``face_id`` runs from **1** through **7** on this network; each id is one face + of the embedding, including the unbounded **exterior face** (here ``face_id = 1`` + borders the outer layout of the city block). +* Every ``edge_id`` appears **twice**, once with ``side = 1`` and once with + ``side = 2``. +* Rows follow the counterclockwise walk of each face in the computed embedding. + +The diagram below sketches the full sample graph. Face boundaries are not drawn +to scale; run example 6) to count how many edge incidences belong to each +``face_id``. + +.. graphviz:: + + graph G { + node [shape=circle;style=filled;width=.5;fixedsize=true;fontsize=8]; + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17 [color=lightgray]; + 5 [pos="0,0!";label="5"]; + 6 [pos="1,0!";label="6"]; + 10 [pos="2,0!";label="10"]; + 15 [pos="3,0!";label="15"]; + 7 [pos="1,1!";label="7"]; + 11 [pos="2,1!";label="11"]; + 14 [pos="3,1!";label="14"]; + 1 [pos="0,2!";label="1"]; + 3 [pos="1,2!";label="3"]; + 2 [pos="2,2!";label="2"]; + 16 [pos="3,2!";label="16"]; + 9 [pos="0,3!";label="9"]; + 12 [pos="1,3!";label="12"]; + 13 [pos="2,3!";label="13"]; + 17 [pos="3,3!";label="17"]; + 8 [pos="1,4!";label="8"]; + 4 [pos="3,4!";label="4"]; + 5 -- 6 [label="1"]; + 6 -- 10 [label="2"]; + 10 -- 15 [label="3"]; + 6 -- 7 [label="4"]; + 10 -- 11 [label="5"]; + 1 -- 3 [label="6"]; + 3 -- 7 [label="7"]; + 7 -- 11 [label="8"]; + 11 -- 16 [label="9"]; + 7 -- 8 [label="10"]; + 11 -- 12 [label="11"]; + 8 -- 12 [label="12"]; + 12 -- 17 [label="13"]; + 8 -- 9 [label="14"]; + 16 -- 17 [label="15"]; + 15 -- 16 [label="16"]; + 2 -- 4 [label="17"]; + 13 -- 14 [label="18"]; + } + +A **triangle** has one bounded interior face and the exterior face. Each of the +three edges appears on both sides (six rows total). + +.. graphviz:: + + graph G { + node [shape=circle;style=filled;width=.5;fixedsize=true;fontsize=8]; + 1,2,3 [color=lightblue]; + 1 -- 2 [label="1"]; + 2 -- 3 [label="2"]; + 3 -- 1 [label="3"]; + } + +A **single edge** separates two regions of the plane; face extraction still +returns two rows (left and right of edge ``1``). + +.. graphviz:: + + graph G { + node [shape=circle;style=filled;width=.5;fixedsize=true;fontsize=8]; + 5,6 [color=lightblue]; + 5 -- 6 [label="1"]; + } + +Parameters +------------------------------------------------------------------------------- + +.. include:: pgRouting-concepts.rst + :start-after: only_edge_param_start + :end-before: only_edge_param_end + +Inner Queries +------------------------------------------------------------------------------- + +Edges SQL +............................................................................... + +.. include:: pgRouting-concepts.rst + :start-after: basic_edges_sql_start + :end-before: basic_edges_sql_end + +Result columns +------------------------------------------------------------------------------- + +.. list-table:: + :width: 81 + :widths: auto + :header-rows: 1 + + * - Column + - Type + - Description + * - ``seq`` + - ``BIGINT`` + - Sequential value starting from 1. + * - ``face_id`` + - ``BIGINT`` + - Identifier of the face in the computed embedding. + * - ``edge_id`` + - ``BIGINT`` + - Identifier of the edge that borders the face. + * - ``side`` + - ``INTEGER`` + - | ``1`` when the edge borders the face on the left side. + | ``2`` when the edge borders the face on the right side. + +Additional Examples +------------------------------------------------------------------------------- + +The examples of this section are based on the :doc:`sampledata` network unless +noted otherwise. + +Sample network overview +............................................................................... + +.. figure:: /images/Fig1-originalData.png + :scale: 50% + + Directed sample network. ``pgr_planarFaces`` treats every edge as undirected, + so ``cost`` and ``reverse_cost`` do not affect face extraction. + +Non-planar graphs +............................................................................... + +Face extraction requires a planar graph. Subgraphs containing a :math:`K_5` or +:math:`K_{3,3}` minor cannot be embedded without crossings; ``pgr_planarFaces`` +raises ``ERROR: Graph is not planar`` in that case. The :doc:`pgr_isPlanar` +documentation shows a non-planar subgraph of ``sampledata`` (edges highlighted in +blue in the figure). + +.. figure:: images/nonPlanar.png + :scale: 50% + + A non-planar subgraph of ``sampledata``. Test with ``pgr_isPlanar`` before + calling ``pgr_planarFaces``. + +1) Check planarity of the full graph ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. literalinclude:: planarFaces.queries + :start-after: -- q2 + :end-before: -- q3 + +.. rubric:: Explanation + +* ``pgr_isPlanar`` returns ``true`` for the full ``edges`` table, so face + extraction in the main example is valid. +* When the result is ``false``, do not call ``pgr_planarFaces`` on the same edge + set unless the graph is edited to remove crossings. + +2) Triangle graph built in SQL ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Example 3 creates a three-edge table ``tri_edges`` and extracts its faces. + +.. graphviz:: + + graph G { + node [shape=circle;style=filled;width=.5;fixedsize=true;fontsize=8]; + 1,2,3 [color=lightblue]; + 1 -- 2 [label="1"]; + 2 -- 3 [label="2"]; + 3 -- 1 [label="3"]; + } + +.. literalinclude:: planarFaces.queries + :start-after: -- q3 + :end-before: -- q4 + +.. rubric:: Explanation + +* Six rows are returned: :math:`2 \times 3` edges. +* ``face_id`` **1** is the interior triangle; ``face_id`` **2** is the exterior + face. +* Each edge lists ``1`` on one face and ``2`` on the other. + +3) Single edge from ``sampledata`` ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Edge ``1`` connects vertices :math:`5` and :math:`6`. + +.. literalinclude:: planarFaces.queries + :start-after: -- q4 + :end-before: -- q5 + +.. rubric:: Explanation + +* Two rows are returned for the single edge (:math:`2|E|` with :math:`|E| = 1`). +* Both incidences use ``face_id = 1`` in this degenerate one-edge graph; the left + and right sides still differ (``1`` vs ``2``). + +4) Row count on three ``sampledata`` edges ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Edges :math:`8`, :math:`10`, and :math:`12` connect vertices +:math:`\{7, 8, 11, 12\}` (see :doc:`sampledata`). + +.. graphviz:: + + graph G { + node [shape=circle;style=filled;width=.5;fixedsize=true;fontsize=8]; + 7,8,11,12 [color=lightblue]; + 7 -- 11 [label="8"]; + 7 -- 8 [label="10"]; + 8 -- 12 [label="12"]; + } + +.. literalinclude:: planarFaces.queries + :start-after: -- q5 + :end-before: -- q6 + +.. rubric:: Explanation + +* The count is **6** (:math:`2 \times 3` edges). +* Three edges on four vertices still yield two faces in the embedding (compare + the six rows in example 2)). + +5) Row count on a larger subgraph ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Edges with ``id < 10`` are the same subgraph used in several metrics-module +examples. + +.. figure:: /images/Fig6-undirected.png + :scale: 50% + + Undirected view of the sample network (subgraph with ``id < 10`` highlighted + in related documentation). + +.. graphviz:: + + graph G { + node [shape=circle;style=filled;width=.5;fixedsize=true;fontsize=8]; + 1,3,5,6,7,10,11,15,16 [color=lightgray]; + 5 [pos="0,0!";label="5"]; + 6 [pos="1,0!";label="6"]; + 10 [pos="2,0!";label="10"]; + 15 [pos="3,0!";label="15"]; + 7 [pos="1,1!";label="7"]; + 11 [pos="2,1!";label="11"]; + 16 [pos="3,2!";label="16"]; + 1 [pos="0,2!";label="1"]; + 3 [pos="1,2!";label="3"]; + 5 -- 6 [label="1"]; + 6 -- 10 [label="2"]; + 10 -- 15 [label="3"]; + 6 -- 7 [label="4"]; + 10 -- 11 [label="5"]; + 1 -- 3 [label="6"]; + 3 -- 7 [label="7"]; + 7 -- 11 [label="8"]; + 11 -- 16 [label="9"]; + } + +.. literalinclude:: planarFaces.queries + :start-after: -- q6 + :end-before: -- q7 + +.. rubric:: Explanation + +* **18** rows means **9** edges in this subgraph (:math:`2 \times 9`). +* The subgraph is planar, so extraction succeeds even though the full network + adds more faces when all edges are included. + +6) Count edge incidences per face ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Aggregate the main example result by ``face_id``. + +.. literalinclude:: planarFaces.queries + :start-after: -- q7 + :end-before: -- q8 + +.. rubric:: Explanation + +* Seven distinct ``face_id`` values appear on the full graph. +* ``edge_count`` is the number of result rows for that ``face_id`` (each physical + edge can contribute up to two rows, one per side). +* ``face_id = 1`` has the largest count because it includes the exterior face + wrapping the drawing. + +Face-walk illustration (subgraph ``id < 10``) +............................................................................... + +The following sketch shows how a face traversal cycles through directed +half-edges. Labels are ``edge_id`` values from ``sampledata``. + +.. graphviz:: + + graph G { + node [shape=circle;style=filled;width=.5;fixedsize=true;fontsize=8]; + 6,7,10,11 [color=lightyellow]; + 6 -- 7 [label="4"]; + 7 -- 11 [label="8"]; + 11 -- 10 [label="5"]; + 10 -- 6 [label="2"]; + } + +7) Empty edge set ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. literalinclude:: planarFaces.queries + :start-after: -- q8 + :end-before: -- q9 + +.. rubric:: Explanation + +* When the inner query returns no edges, a notice is emitted and the result is + empty. +* No exception is raised. + +8) Each edge borders exactly two faces ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Every undirected edge must appear once with ``side = 1`` and once with +``side = 2``. The query below lists edges that violate this rule (none on the +full sample graph). + +.. literalinclude:: planarFaces.queries + :start-after: -- q9 + :end-before: -- q10 + +.. rubric:: Explanation + +* An empty result confirms each ``edge_id`` appears exactly twice in the output. +* This is the same property verified in ``pgtap/planar/planarFaces/edge_cases.pg``. + +9) Face row count on edges ``id < 10`` ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. literalinclude:: planarFaces.queries + :start-after: -- q10 + :end-before: -- q11 + +.. rubric:: Explanation + +* The count **18** matches example 5): the same subgraph yields the same + :math:`2|E|` row count. +* Together with example 1), this confirms the :math:`2|E|` row rule on a + nine-edge subgraph. +* Confirm planarity with :doc:`pgr_isPlanar` before calling ``pgr_planarFaces`` on + the same ``Edges SQL``. + + +See Also +------------------------------------------------------------------------------- + +* :doc:`pgr_isPlanar` +* :doc:`sampledata` +* `Boost: Planar Face Traversal + `__ +* `Boost: Boyer Myrvold planarity + `__ +* Wikipedia: `Planar graph + `__ + +.. rubric:: Indices and tables + +* :ref:`genindex` +* :ref:`search` + diff --git a/doc/planar/planar-family.rst b/doc/planar/planar-family.rst new file mode 100644 index 00000000000..2bd6af21700 --- /dev/null +++ b/doc/planar/planar-family.rst @@ -0,0 +1,37 @@ +:file: This file is part of the pgRouting project. +:copyright: Copyright (c) 2026-2026 pgRouting developers +:license: Creative Commons Attribution-Share Alike 3.0 https://creativecommons.org/licenses/by-sa/3.0 + + +.. index:: Planar Family + +| + +Planar - Family of functions +=============================================================================== + +.. include:: experimental.rst + :start-after: warning-begin + :end-before: end-warning + +.. experimental-start + +* :doc:`pgr_isPlanar` - Returns whether the graph is planar. +* :doc:`pgr_planarFaces` - Identifies the faces of a planar embedding and lists + every edge-face incidence for an undirected graph. + +.. experimental-end + +.. toctree:: + :hidden: + + pgr_isPlanar + pgr_planarFaces + +See Also +------------------------------------------------------------------------------- + +.. rubric:: Indices and tables + +* :ref:`genindex` +* :ref:`search` diff --git a/doc/src/experimental.rst b/doc/src/experimental.rst index 859a8cb19b4..6602537070f 100644 --- a/doc/src/experimental.rst +++ b/doc/src/experimental.rst @@ -127,14 +127,16 @@ Experimental Functions pgr_dagShortestPath pgr_edwardMoore -.. rubric:: Planar Family +:doc:`planar-family` -- :doc:`pgr_isPlanar` +.. include:: planar-family.rst + :start-after: experimental-start + :end-before: experimental-end .. toctree:: :hidden: - pgr_isPlanar + planar-family .. rubric:: Miscellaneous Algorithms From ef1d3af1da15b3775d182ecc2a3dbe5009a074e5 Mon Sep 17 00:00:00 2001 From: sakirr05 Date: Sat, 22 Aug 2026 05:49:43 +0530 Subject: [PATCH 6/6] Updating release notes and NEWS about the new function pgr_planarFaces --- NEWS.md | 4 ++++ doc/_static/page_history.js | 2 ++ doc/src/release_notes.rst | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/NEWS.md b/NEWS.md index e4dc47bb984..a915f6fa6ef 100644 --- a/NEWS.md +++ b/NEWS.md @@ -37,6 +37,10 @@ To see all issues & pull requests closed by this release see the **Summary of changes by function** +* pgr_planarFaces + + * New experimental function. + * pgr_edgeColoring * Fix the way it builds the graph diff --git a/doc/_static/page_history.js b/doc/_static/page_history.js index f158646394d..a4cd13fc251 100644 --- a/doc/_static/page_history.js +++ b/doc/_static/page_history.js @@ -16,6 +16,8 @@ var titles = [ var newpages = [ + {v: '4.1', pages: ['pgr_planarFaces', 'planar-family']}, + {v: '4.0', pages: ['pgr_bandwidth', 'pgr_kingOrdering', 'pgr_sloanOrdering']}, {v: '3.8', pages: ['pgr_contractionDeadEnd', 'pgr_contractionLinear', 'pgr_separateCrossing', diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index 40390c34c70..dabd62db1a0 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -62,6 +62,12 @@ To see all issues & pull requests closed by this release see the .. rubric:: Summary of changes by function +* pgr_planarFaces + + .. include:: pgr_planarFaces.rst + :start-after: Version 4.1.0 + :end-before: Description + * pgr_edgeColoring .. include:: pgr_edgeColoring.rst