diff --git a/doc/planar/pgr_makeMaximalPlanar.rst b/doc/planar/pgr_makeMaximalPlanar.rst index 9373dad4ff..e5aeef56f8 100644 --- a/doc/planar/pgr_makeMaximalPlanar.rst +++ b/doc/planar/pgr_makeMaximalPlanar.rst @@ -4,14 +4,14 @@ .. index:: single: Planar Family ; pgr_makeMaximalPlanar - Experimental - single: makeMaximalPlanar - Experimental on v4.2 + single: makeMaximalPlanar - Experimental on v4.1 | ``pgr_makeMaximalPlanar`` - Experimental =============================================================================== -``pgr_makeMaximalPlanar`` — [TBD] +``pgr_makeMaximalPlanar`` — Returns the set of edges needed to make a planar graph maximal planar. .. include:: experimental.rst :start-after: warning-begin @@ -19,7 +19,7 @@ .. rubric:: Availability -.. rubric:: Version 4.2.0 +.. rubric:: Version 4.1.0 * New experimental function. @@ -27,4 +27,90 @@ Description ------------------------------------------------------------------------------- -[TBD] +A graph is planar if it can be drawn in two-dimensional space with no two of its +edges crossing. A planar graph is considered **maximal planar** (or fully triangulated) +if no additional edges can be added to it without violating its planarity. In a +maximal planar graph, every face (including the outer face) is a triangle. + +``pgr_makeMaximalPlanar`` identifies the missing edges that need to be added to an +existing planar graph to make it maximal planar. + +The main characteristics are: + +* Works for **undirected** graphs. +* Returns a list of all new edges which are needed to triangulate the graph and make it maximal planar. +* If the input graph is not planar, it returns an empty set. +* The algorithm does not consider traversal costs in the calculations. +* The algorithm does not consider geometric topology in the calculations. +* Running time: :math:`O(|V| + |E|)` + +|Boost| Boost Graph Inside + +Signatures +------------------------------------------------------------------------------- + +.. admonition:: \ \ + :class: signatures + + | pgr_makeMaximalPlanar(`Edges SQL`_) + + | Returns set of |result-component-make| + | OR EMPTY SET + +:Example: List of edges that are needed to make the graph maximal planar. + +.. literalinclude:: makeMaximalPlanar.queries + :start-after: -- q1 + :end-before: -- q2 + +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 +------------------------------------------------------------------------------- + +Returns set of |result-component-make| + +.. list-table:: + :width: 81 + :widths: auto + :header-rows: 1 + + * - Column + - Type + - Description + * - ``seq`` + - ``BIGINT`` + - Sequential value starting from **1**. + * - ``start_vid`` + - ``BIGINT`` + - Identifier of the first end point vertex of the edge. + * - ``end_vid`` + - ``BIGINT`` + - Identifier of the second end point vertex of the edge. + +See Also +------------------------------------------------------------------------------- + +* `Boost: make_maximal_planar + `__ +* :doc:`sampledata` + +.. rubric:: Indices and tables + +* :ref:`genindex` +* :ref:`search` diff --git a/docqueries/planar/makeMaximalPlanar.result b/docqueries/planar/makeMaximalPlanar.result index 3767ff8b1b..b20cf64257 100644 --- a/docqueries/planar/makeMaximalPlanar.result +++ b/docqueries/planar/makeMaximalPlanar.result @@ -1,3 +1,35 @@ +BEGIN; +BEGIN +SET client_min_messages TO NOTICE; +SET /* :file: This file is part of the pgRouting project. :copyright: Copyright (c) 2020-2026 pgRouting developers :license: Creative Commons Attribution-Share Alike 3.0 https://creativecommons.org/licenses/by-sa/3.0 */ +/* -- q1 */ +SELECT * FROM pgr_makeMaximalPlanar( + 'SELECT id, source, target, cost, reverse_cost FROM edges' +); + seq | start_vid | end_vid +-----+-----------+--------- + 1 | 7 | 9 + 2 | 10 | 5 + 3 | 3 | 11 + 4 | 1 | 7 + 5 | 1 | 6 + 6 | 1 | 5 + 7 | 1 | 10 + 8 | 1 | 11 + 9 | 17 | 15 + 10 | 17 | 10 + 11 | 17 | 6 + 12 | 17 | 7 + 13 | 17 | 8 + 14 | 15 | 11 + 15 | 9 | 11 + 16 | 9 | 12 + 17 | 16 | 12 +(17 rows) + +/* -- q2 */ +ROLLBACK; +ROLLBACK diff --git a/docqueries/planar/test.conf b/docqueries/planar/test.conf index 3c33912e3d..96e526c790 100644 --- a/docqueries/planar/test.conf +++ b/docqueries/planar/test.conf @@ -7,6 +7,7 @@ 'any' => { 'files' => [qw( isPlanar.pg + makeMaximalPlanar.pg )] }, diff --git a/include/planar/makeMaximalPlanar.hpp b/include/planar/makeMaximalPlanar.hpp index cb0c612a36..b895027186 100644 --- a/include/planar/makeMaximalPlanar.hpp +++ b/include/planar/makeMaximalPlanar.hpp @@ -43,6 +43,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. #include #include "c_types/ii_t_rt.h" +#include "cpp_common/edge_t.hpp" #include "cpp_common/messages.hpp" #include "cpp_common/base_graph.hpp" #include "cpp_common/interruption.hpp" @@ -58,7 +59,47 @@ class Pgr_makeMaximalPlanar : public pgrouting::Pgr_messages { typedef typename G::E_i E_i; std::vector makeMaximalPlanar(G &graph) { - return generateMakeMaximalPlanar(graph); + /* Find how many connected components this graph has */ + std::vector component(boost::num_vertices(graph.graph)); + auto num_components = boost::connected_components( + graph.graph, &component[0]); + + if (num_components == 1) { + /* Simple case: single connected graph — process directly */ + return generateMakeMaximalPlanar(graph); + } + + /* Multi-component case: split the graph into connected sub-graphs */ + log << "Graph has " << num_components + << " connected components. Processing each independently.\n"; + + /* Collect edges per component using vertex component labels */ + std::vector> comp_edges(num_components); + E_i ei, ei_end; + for (boost::tie(ei, ei_end) = edges(graph.graph); + ei != ei_end; ++ei) { + V src_v = boost::source(*ei, graph.graph); + size_t c = component[src_v]; + Edge_t e; + e.id = graph[*ei].id; + e.source = graph[src_v].id; + e.target = graph[boost::target(*ei, graph.graph)].id; + e.cost = graph[*ei].cost; + e.reverse_cost = -1; + comp_edges[c].push_back(e); + } + + std::vector all_results; + for (size_t c = 0; c < num_components; ++c) { + if (comp_edges[c].empty()) continue; + G sub_graph; + sub_graph.insert_edges(comp_edges[c]); + auto sub_results = generateMakeMaximalPlanar(sub_graph); + all_results.insert( + all_results.end(), + sub_results.begin(), sub_results.end()); + } + return all_results; } private: @@ -119,12 +160,6 @@ class Pgr_makeMaximalPlanar : public pgrouting::Pgr_messages { return std::vector(); } - std::vector component(boost::num_vertices(graph.graph)); - auto num_components = boost::connected_components(graph.graph, &component[0]); - if (num_components > 1) { - throw std::string("Graph is not connected. Please run pgr_makeConnected first."); - } - std::vector results; planar_visitor vis(results, graph); diff --git a/locale/en/LC_MESSAGES/pgrouting_doc_strings.po b/locale/en/LC_MESSAGES/pgrouting_doc_strings.po index a04ece1470..e441b472cb 100644 --- a/locale/en/LC_MESSAGES/pgrouting_doc_strings.po +++ b/locale/en/LC_MESSAGES/pgrouting_doc_strings.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: pgRouting v4.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-05 13:26+0000\n" +"POT-Creation-Date: 2026-06-16 15:22+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -12897,10 +12897,54 @@ msgid "" msgstr "" #, fuzzy -msgid "pgr_makeMaximalPlanar" +msgid "``pgr_makeMaximalPlanar`` - Experimental" msgstr "pgr_isPlanar" -msgid "(Documentation to be added later)" +msgid "" +"``pgr_makeMaximalPlanar`` — Returns the set of edges needed to make a planar " +"graph maximal planar." +msgstr "" + +msgid "" +"A graph is planar if it can be drawn in two-dimensional space with no two of " +"its edges crossing. A planar graph is considered **maximal planar** (or " +"fully triangulated) if no additional edges can be added to it without " +"violating its planarity. In a maximal planar graph, every face (including " +"the outer face) is a triangle." +msgstr "" + +msgid "" +"``pgr_makeMaximalPlanar`` identifies the missing edges that need to be added " +"to an existing planar graph to make it maximal planar." +msgstr "" + +msgid "" +"Returns a list of all new edges which are needed to triangulate the graph " +"and make it maximal planar." +msgstr "" + +msgid "If the input graph is not planar, it returns an empty set." +msgstr "" + +msgid "The algorithm does not consider traversal costs in the calculations." +msgstr "" + +msgid "The algorithm does not consider geometric topology in the calculations." +msgstr "" + +msgid "Running time: :math:`O(|V| + |E|)`" +msgstr "" + +#, fuzzy +msgid "pgr_makeMaximalPlanar(`Edges SQL`_)" +msgstr "pgr_isPlanar" + +msgid "List of edges that are needed to make the graph maximal planar." +msgstr "" + +msgid "" +"`Boost: make_maximal_planar `__" msgstr "" msgid "``pgr_maxCardinalityMatch``" diff --git a/locale/pot/pgrouting_doc_strings.pot b/locale/pot/pgrouting_doc_strings.pot index 4c3c7e080b..4a9750396c 100644 --- a/locale/pot/pgrouting_doc_strings.pot +++ b/locale/pot/pgrouting_doc_strings.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: pgRouting v4.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-05 13:26+0000\n" +"POT-Creation-Date: 2026-06-16 15:22+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -10990,10 +10990,40 @@ msgstr "" msgid "`Boost: make connected `__" msgstr "" -msgid "pgr_makeMaximalPlanar" +msgid "``pgr_makeMaximalPlanar`` - Experimental" msgstr "" -msgid "(Documentation to be added later)" +msgid "``pgr_makeMaximalPlanar`` — Returns the set of edges needed to make a planar graph maximal planar." +msgstr "" + +msgid "A graph is planar if it can be drawn in two-dimensional space with no two of its edges crossing. A planar graph is considered **maximal planar** (or fully triangulated) if no additional edges can be added to it without violating its planarity. In a maximal planar graph, every face (including the outer face) is a triangle." +msgstr "" + +msgid "``pgr_makeMaximalPlanar`` identifies the missing edges that need to be added to an existing planar graph to make it maximal planar." +msgstr "" + +msgid "Returns a list of all new edges which are needed to triangulate the graph and make it maximal planar." +msgstr "" + +msgid "If the input graph is not planar, it returns an empty set." +msgstr "" + +msgid "The algorithm does not consider traversal costs in the calculations." +msgstr "" + +msgid "The algorithm does not consider geometric topology in the calculations." +msgstr "" + +msgid "Running time: :math:`O(|V| + |E|)`" +msgstr "" + +msgid "pgr_makeMaximalPlanar(`Edges SQL`_)" +msgstr "" + +msgid "List of edges that are needed to make the graph maximal planar." +msgstr "" + +msgid "`Boost: make_maximal_planar `__" msgstr "" msgid "``pgr_maxCardinalityMatch``" diff --git a/pgtap/planar/makeMaximalPlanar/edge_cases.pg b/pgtap/planar/makeMaximalPlanar/edge_cases.pg new file mode 100644 index 0000000000..fd3078cb62 --- /dev/null +++ b/pgtap/planar/makeMaximalPlanar/edge_cases.pg @@ -0,0 +1,138 @@ +/* :file: This file is part of the pgRouting project. +:copyright: Copyright (c) 2018-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; + +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; + +-- 0 edge, 0 vertex tests + +PREPARE q1 AS +SELECT id, source, target, cost, reverse_cost +FROM edges +WHERE id > 18; + +-- Graph is empty - it has 0 edge and 0 vertex +RETURN QUERY +SELECT is_empty('q1', 'q1: Graph with 0 edge and 0 vertex'); + +PREPARE zeroEdgeTest2 AS +SELECT * +FROM pgr_makeMaximalPlanar('q1'); + +RETURN QUERY +SELECT is_empty('zeroEdgeTest2', '2: Empty, since graph is empty'); + + +-- 1 vertex test + +PREPARE q5 AS +SELECT id, source, source AS target, cost, reverse_cost +FROM edges +WHERE id = 9; + +PREPARE oneVertexTest6 AS +SELECT * +FROM pgr_makeMaximalPlanar('q5'); + +RETURN QUERY +SELECT is_empty('oneVertexTest6', '6: Empty, since graph has 1 vertex'); + + +-- 2 vertices tests + +PREPARE q9 AS +SELECT id, source, target, cost, reverse_cost +FROM edges +WHERE id = 1; + +PREPARE twoVerticesTest10 AS +SELECT * +FROM pgr_makeMaximalPlanar('q9'); + +RETURN QUERY +SELECT is_empty('twoVerticesTest10', '10: Empty, graph has 2 vertices'); + + +-- 3 vertices test (Path graph: 5-6-10) + +PREPARE q11 AS +SELECT id, source, target, cost, reverse_cost +FROM edges +WHERE id IN (1,2); + +PREPARE threeVerticesTest12 AS +SELECT start_vid, end_vid +FROM pgr_makeMaximalPlanar('q11'); + +-- Path graph 5-6-10 needs edge 5-10 to become maximal planar +RETURN QUERY +SELECT set_eq('threeVerticesTest12', $$VALUES(5::bigint, 10::bigint) $$, '12: P3 graph bridged'); + + +-- 4 vertices test (Cyclic, need 1 more edge to triangulate to maximal planar) + +PREPARE q15 AS +SELECT id, source, target, cost, reverse_cost +FROM edges +WHERE id IN (8, 10, 11, 12); + +PREPARE fourVerticesCyclicTest16 AS +SELECT start_vid, end_vid +FROM pgr_makeMaximalPlanar('q15'); + +RETURN QUERY +SELECT isnt_empty('fourVerticesCyclicTest16', '16: Not empty, cyclic graph needs 1 edge to become maximal planar'); + + +-- Disjoint graphs (P3 + Cyclic) + +PREPARE q17 AS +SELECT id, source, target, cost, reverse_cost +FROM edges +WHERE id IN (1,2, 8,10,11,12); + +PREPARE disjointTest18 AS +SELECT start_vid, end_vid +FROM pgr_makeMaximalPlanar('q17'); + +RETURN QUERY +SELECT isnt_empty('disjointTest18', '18: Disjoint graph processed independently'); + + +-- Non-planar graph (K5) +PREPARE q19 AS +SELECT id, source, target, cost, reverse_cost FROM (VALUES + (1, 1, 2, 1.0, -1.0), (2, 1, 3, 1.0, -1.0), (3, 1, 4, 1.0, -1.0), (4, 1, 5, 1.0, -1.0), + (5, 2, 3, 1.0, -1.0), (6, 2, 4, 1.0, -1.0), (7, 2, 5, 1.0, -1.0), + (8, 3, 4, 1.0, -1.0), (9, 3, 5, 1.0, -1.0), + (10, 4, 5, 1.0, -1.0) +) AS t(id, source, target, cost, reverse_cost); + +PREPARE nonPlanarTest20 AS +SELECT * FROM pgr_makeMaximalPlanar('q19'); + +RETURN QUERY +SELECT is_empty('nonPlanarTest20', '20: Non-planar graph returns empty'); + +END; +$BODY$ +LANGUAGE plpgsql; + +SELECT edge_cases(); + +SELECT * FROM finish(); +ROLLBACK; diff --git a/pgtap/planar/makeMaximalPlanar/inner_query.pg b/pgtap/planar/makeMaximalPlanar/inner_query.pg new file mode 100644 index 0000000000..6d4364e815 --- /dev/null +++ b/pgtap/planar/makeMaximalPlanar/inner_query.pg @@ -0,0 +1,31 @@ +/* :file: This file is part of the pgRouting project. +:copyright: Copyright (c) 2020-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_makeMaximalPlanar(', ')'); + +END; +$BODY$ +LANGUAGE plpgsql; + +SELECT inner_query(); + +SELECT finish(); +ROLLBACK; diff --git a/pgtap/planar/makeMaximalPlanar/no_crash_test.pg b/pgtap/planar/makeMaximalPlanar/no_crash_test.pg new file mode 100644 index 0000000000..4cd8b022cf --- /dev/null +++ b/pgtap/planar/makeMaximalPlanar/no_crash_test.pg @@ -0,0 +1,44 @@ +/* :file: This file is part of the pgRouting project. +:copyright: Copyright (c) 2018-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 (5) ELSE plan(1) END; + +PREPARE edges AS +SELECT id, source, target, cost, reverse_cost FROM edges; + +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'); + + params = ARRAY['$$SELECT id, source, target, cost, reverse_cost FROM edges$$']::TEXT[]; + subs = ARRAY[ + 'NULL' + ]::TEXT[]; + + RETURN QUERY + SELECT * FROM no_crash_test('pgr_makemaximalplanar', params, subs); + +END +$BODY$ +LANGUAGE plpgsql VOLATILE; + + +SELECT * FROM test_function(); +SELECT finish(); +ROLLBACK; diff --git a/pgtap/planar/makeMaximalPlanar/types_check.pg b/pgtap/planar/makeMaximalPlanar/types_check.pg new file mode 100644 index 0000000000..45d0848113 --- /dev/null +++ b/pgtap/planar/makeMaximalPlanar/types_check.pg @@ -0,0 +1,41 @@ +/* :file: This file is part of the pgRouting project. +:copyright: Copyright (c) 2018-2026 pgRouting developers +:license: Creative Commons Attribution-Share Alike 3.0 https://creativecommons.org/licenses/by-sa/3.0 */ + +BEGIN; + +SELECT CASE WHEN NOT min_version('4.1.0') THEN plan(1) ELSE plan(4) 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_makemaximalplanar'); + RETURN QUERY SELECT function_returns('pgr_makemaximalplanar', ARRAY['text'], 'setof record'); + + RETURN QUERY + SELECT function_args_eq('pgr_makemaximalplanar', + $$SELECT '{"",seq,start_vid,end_vid}'::TEXT[] $$ + ); + + RETURN QUERY + SELECT function_types_eq('pgr_makemaximalplanar', + $$VALUES + ('{text,int8,int8,int8}'::TEXT[]) + $$ + ); +END; +$BODY$ +LANGUAGE plpgsql; + +SELECT types_check(); + +SELECT * FROM finish(); +ROLLBACK;