Vous regroupez des chaînes de lignes connectées dans PostGIS?

12

J'ai un tableau des rues que j'ai sélectionné en fonction d'un ensemble d'attributs (disons que c'est speed_limit < 25). Il y a des groupes de rues qui sont localement contiguës; Je voudrais regrouper ces ensembles de chaînes de lignes connectées dans GeometryCollections. Dans l'image ci-dessous, il y aurait deux GeometryCollections: une avec les lignes rouges et une avec les lignes bleues.

entrez la description de l'image ici

J'ai essayé d'exécuter quelques requêtes "dissoudre, désagréger" selon les lignes suivantes:

SELECT (ST_Dump(st_union)).geom
FROM 
    (SELECT ST_Union(geom) FROM roads) sq

Avec tout ce que j'ai essayé, je me retrouve avec une seule fonction ( ST_Union) ou ma géométrie d'origine ( ST_Dumpde ST_Union).

Peut-être qu'il est possible de faire cela avec une sorte de WITH RECURSIVEmagie?

dbaston
la source
Quelque chose ne va pas bien avec "(ST_Dump (st_union)). Geom"
Martin F
Comme il ne s'appelait pas ST_Union (geom), le nom du nouveau geom a hérité du nom de la fonction pour devenir st_union. C'est pourquoi ça a l'air un peu drôle
LR1234567

Réponses:

19

Donc, par exemple. Voici un tableau simple avec deux groupes d'arêtes connectés:

drop table lines;
create table lines ( id integer primary key, geom geometry(linestring) );
insert into lines (id, geom) values ( 1, 'LINESTRING(0 0, 0 1)');
insert into lines (id, geom) values ( 2, 'LINESTRING(0 1, 1 1)');
insert into lines (id, geom) values ( 3, 'LINESTRING(1 1, 1 2)');
insert into lines (id, geom) values ( 4, 'LINESTRING(1 2, 2 2)');
insert into lines (id, geom) values ( 11, 'LINESTRING(10 10, 10 11)');
insert into lines (id, geom) values ( 12, 'LINESTRING(10 11, 11 11)');
insert into lines (id, geom) values ( 13, 'LINESTRING(11 11, 11 12)');
insert into lines (id, geom) values ( 14, 'LINESTRING(11 12, 12 12)');
create index lines_gix on lines using gist(geom);

Maintenant, voici une fonction récursive qui, étant donné l'id d'un bord, accumule tous les bords qui se touchent:

CREATE OR REPLACE FUNCTION find_connected(integer) returns integer[] AS
$$
WITH RECURSIVE lines_r AS (
  SELECT ARRAY[id] AS idlist, geom, id
  FROM lines 
  WHERE id = $1
  UNION ALL
  SELECT array_append(lines_r.idlist, lines.id) AS idlist, 
         lines.geom AS geom, 
         lines.id AS id
  FROM lines, lines_r
  WHERE ST_Touches(lines.geom, lines_r.geom)
  AND NOT lines_r.idlist @> ARRAY[lines.id]
)
SELECT 
  array_agg(id) AS idlist
  FROM lines_r
$$ 
LANGUAGE 'sql';

Cela nous laisse juste besoin de trouver, après que chaque groupe est accumulé, l'identifiant d'un bord qui ne fait pas déjà partie d'un groupe. Ce qui, tragiquement, nécessite une deuxième requête récursive.

WITH RECURSIVE groups_r AS (
  (SELECT find_connected(id) AS idlist, 
          find_connected(id) AS grouplist, 
          id FROM lines WHERE id = 1)
  UNION ALL
  (SELECT array_cat(groups_r.idlist,find_connected(lines.id)) AS idlist,
         find_connected(lines.id) AS grouplist,
         lines.id
  FROM lines, groups_r
  WHERE NOT idlist @> ARRAY[lines.id]
  LIMIT 1)
)
SELECT id, grouplist
FROM groups_r;   

Ce qui, pris ensemble, renvoie un joli ensemble avec l'identifiant de la graine et chaque groupe qu'il a accumulé. Je laisse le soin au lecteur de transformer les tableaux d'id en requête pour créer une géométrie pour la cartographie.

 id |   grouplist   
----+---------------
  1 | {1,2,3,4}
 11 | {11,12,13,14}
(2 rows)
Paul Ramsey
la source
Je pense que ce code pourrait être plus simple, si le type de géométrie prenant en charge le hachage dans PostgreSQL (lorsque vous écrivez un RCTE plus simple qui n'implique pas d'accumuler des tableaux d'ID, vous obtenez une erreur "Tous les types de données de colonne doivent être lavables"), donc il y a un petite demande d'amélioration pour moi.
Paul Ramsey
C'est une approche vraiment géniale. Je remarque des résultats étranges en l'appliquant à un ensemble de test plus grand; Je vais voir si je peux réduire le problème à un exemple simple. 100 lignes: 85 grappes, plus grande grappe = 3, 0,03 s //// 200 lignes: 144 grappes, plus grande grappe = 9, 0,08 s //// 300 lignes: 180 grappes, plus grande grappe = 51, 0,16 s /// / 400 lignes: 188 grappes, plus grande grappe = 41, 0,27 s //// 500 lignes: 176 grappes, plus grande grappe = 112, 0,56 s //// 600 lignes: 143 grappes, plus grande grappe = 449, 1,0 s // // 650 lignes: 133 grappes, plus grande grappe = 7601, 6,8 s
dbaston
L' ajout de ce aux données de test entraînera des ID en double dans le grouplisttableau: insert into lines (id, geom) values ( 15, 'LINESTRING(0 0, 10 10)');. La modification array_agg(id)de la fonction de retour à array_agg(DISTINCT id)semble résoudre le problème.
dbaston
C'est une bonne solution, alors comment pouvons-nous stocker les géométries dans une table afin que nous puissions voir les lignes connectées?
zakaria mouqcit
6

Voici une approche qui utilise une table temporaire pour agréger progressivement des clusters ensemble. Je ne me soucie pas vraiment de l'approche de table temporaire, mais cela semble fonctionner assez bien à mesure que le nombre de lignes augmente (j'ai 1,2 M de lignes en entrée).

DO
$$
DECLARE
this_id bigint;
this_geom geometry;
cluster_id_match integer;

id_a bigint;
id_b bigint;

BEGIN
DROP TABLE IF EXISTS clusters;
CREATE TABLE clusters (cluster_id serial, ids bigint[], geom geometry);
CREATE INDEX ON clusters USING GIST(geom);

-- Iterate through linestrings, assigning each to a cluster (if there is an intersection)
-- or creating a new cluster (if there is not)
FOR this_id, this_geom IN SELECT id, geom FROM lines LOOP
  -- Look for an intersecting cluster.  (There may be more than one.)
  SELECT cluster_id FROM clusters WHERE ST_Intersects(this_geom, clusters.geom)
     LIMIT 1 INTO cluster_id_match;

  IF cluster_id_match IS NULL THEN
     -- Create a new cluster
     INSERT INTO clusters (ids, geom) VALUES (ARRAY[this_id], this_geom);
  ELSE
     -- Append line to existing cluster
     UPDATE clusters SET geom = ST_Union(this_geom, geom),
                          ids = array_prepend(this_id, ids)
      WHERE clusters.cluster_id = cluster_id_match;
  END IF;
END LOOP;

-- Iterate through the clusters, combining clusters that intersect each other
LOOP
    SELECT a.cluster_id, b.cluster_id FROM clusters a, clusters b 
     WHERE ST_Intersects(a.geom, b.geom)
       AND a.cluster_id < b.cluster_id
      INTO id_a, id_b;

    EXIT WHEN id_a IS NULL;
    -- Merge cluster A into cluster B
    UPDATE clusters a SET geom = ST_Union(a.geom, b.geom), ids = array_cat(a.ids, b.ids)
      FROM clusters b
     WHERE a.cluster_id = id_a AND b.cluster_id = id_b;

    -- Remove cluster B
    DELETE FROM clusters WHERE cluster_id = id_b;
END LOOP;
END;
$$ language plpgsql;
dbaston
la source
fonctionne parfaitement
zakaria mouqcit
@zakariamouqcit Heureux que cela ait fonctionné pour vous! J'ai écrit cette réponse avant d'écrire la ST_ClusterIntersectingfonction dans PostGIS. Si vos données sont suffisamment petites pour tenir en mémoire, je vous suggère de vérifier cela pour une solution plus performante.
dbaston
la recherche de cette question m'a amené ici. J'ai essayé l'itératif et le st_clusterintersecting mais j'ai trouvé que st_clusterDBScan était le plus approprié. Au cas où quelqu'un d'autre serait amené ici aussi. postgis.net/docs/manual-dev/ST_ClusterDBSCAN.html
D_C
D'accord, ST_ClusterDBSCAN est presque toujours la meilleure voie à suivre pour PostGIS 2.3+
dbaston