Verbundene Linestrings in PostGIS gruppieren?

12

Ich habe eine Straßentabelle, die ich basierend auf einer Reihe von Attributen ausgewählt habe (sagen wir, es ist speed_limit < 25). Es gibt Gruppen von Straßen, die lokal benachbart sind. Ich möchte diese Sätze verbundener Linienfolgen in GeometryCollections gruppieren. In der Abbildung unten würde es zwei GeometryCollections geben: eine mit den roten Linien und eine mit den blauen Linien.

Bildbeschreibung hier eingeben

Ich habe versucht, ein paar "Auflösen, Deaggregieren" -Abfragen auszuführen, die wie folgt lauten:

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

Nach allem, was ich ausprobiert habe, habe ich entweder ein einzelnes Feature ( ST_Union) oder meine ursprüngliche Geometrie ( ST_Dumpvon ST_Union).

Vielleicht ist es möglich, dies mit einer Art WITH RECURSIVEMagie zu tun ?

dbaston
quelle
Etwas sieht nicht richtig aus mit "(ST_Dump (st_union)). Geom"
Martin F
Da er ST_Union (geom) nicht als Alias ​​verwendet hat, hat der Name des neuen Geoms den Namen der Funktion geerbt, die st_union werden soll. Das ist, warum es ein wenig lustig aussieht
LR1234567

Antworten:

19

Also zum Beispiel. Hier ist eine einfache Tabelle mit zwei verbundenen Kantengruppen:

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);

Hier ist eine rekursive Funktion, die anhand der ID einer Kante alle Kanten sammelt, die sich berühren:

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';

Deshalb müssen wir nach jeder Gruppe die ID einer Kante finden, die noch nicht Teil einer Gruppe ist. Was tragischerweise eine zweite rekursive Abfrage erfordert.

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;   

Was zusammengenommen ein nettes Set mit der Startnummer und jeder Gruppe ergibt, die es angesammelt hat. Ich überlasse es dem Leser als Übung, die Arrays von id wieder in eine Abfrage umzuwandeln, um Geometrie für das Mapping zu erstellen.

 id |   grouplist   
----+---------------
  1 | {1,2,3,4}
 11 | {11,12,13,14}
(2 rows)
Paul Ramsey
quelle
Ich denke, dieser Code könnte einfacher sein, wenn der Geometrietyp das Hashing in PostgreSQL unterstützt (wenn Sie eine einfachere RCTE schreiben, bei der keine Arrays von IDs akkumuliert werden, wird die Fehlermeldung "Alle Spaltendatentypen müssen hashbar sein" angezeigt) kleiner Verbesserungswunsch für mich.
Paul Ramsey
Dies ist ein wirklich großartiger Ansatz. Ich bemerke einige merkwürdige Ergebnisse, wenn ich sie auf ein größeres Test-Set übertrage. Ich werde sehen, ob ich das Problem auf ein einfaches Beispiel reduzieren kann. 100 Zeilen: 85 Cluster, größter Cluster = 3, 0,03 s //// 200 Zeilen: 144 Cluster, größter Cluster = 9, 0,08 s //// 300 Zeilen: 180 Cluster, größter Cluster = 51, 0,16 s /// / 400 Zeilen: 188 Cluster, größter Cluster = 41, 0,27 s //// 500 Zeilen: 176 Cluster, größter Cluster = 112, 0,56 s //// 600 Zeilen: 143 Cluster, größter Cluster = 449, 1,0 s // // 650 Zeilen: 133 Cluster, größter Cluster = 7601, 6,8 s
dbaston
Addiert man diese zu den Testdaten verursachen doppelte IDs im grouplistArray: insert into lines (id, geom) values ( 15, 'LINESTRING(0 0, 10 10)');. Das Ändern array_agg(id)der Funktion "Zurück zu" array_agg(DISTINCT id)scheint das Problem zu beheben.
Dbaston
Dies ist eine gute Lösung. Wie können wir nun die Geometrien in einer Tabelle speichern, damit wir die verbundenen Linien sehen können?
Zakaria Mouqcit
6

Hier ist ein Ansatz, der eine temporäre Tabelle verwendet, um Cluster inkrementell zusammenzufassen. Ich mag den temporären Tabellenansatz nicht wirklich, aber dies scheint recht gut zu funktionieren, wenn die Anzahl der Zeilen zunimmt (ich habe 1,2 Millionen Zeilen in meiner Eingabe).

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
quelle
funktioniert perfekt
zakaria mouqcit
@zakariamouqcit Ich bin froh, dass das für dich funktioniert hat! Ich habe diese Antwort geschrieben, bevor ich die ST_ClusterIntersectingFunktion in PostGIS geschrieben habe. Wenn Ihre Daten so klein sind, dass sie in den Arbeitsspeicher passen, empfiehlt es sich, eine leistungsfähigere Lösung zu finden.
Dbaston
Die Suche nach dieser Frage brachte mich hierher. Versuchte die iterative und die st_clusterintersecting, fand aber st_clusterDBS am besten. Falls noch jemand hierher gebracht wird. postgis.net/docs/manual-dev/ST_ClusterDBSCAN.html
D_C
Einverstanden, ST_ClusterDBSCAN ist fast immer der beste Weg für PostGIS 2.3+
dbaston