Raggruppare i linestring collegati in PostGIS?


12

Ho una tabella di strade che ho selezionato in base a una serie di attributi (diciamo che è speed_limit < 25). Ci sono gruppi di strade localmente contigui; Vorrei raggruppare questi set di linestring collegati in GeometryCollections. Nell'immagine seguente, ci sarebbero due GeometryCollections: uno con le linee rosse e uno con le linee blu.

inserisci qui la descrizione dell'immagine

Ho provato a eseguire un paio di query "dissolve, deaggregate" sulla falsariga di:

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

Con tutto ciò che ho provato, finisco con una singola funzione ( ST_Union) o con la mia geometria originale ( ST_Dumpdi ST_Union).

Forse è possibile farlo con un qualche tipo di WITH RECURSIVEmagia?


Qualcosa non sembra giusto con "(ST_Dump (st_union)). Geom"
Martin F

Poiché non alias ST_Union (geom) il nome del nuovo geom ha ereditato il nome della funzione per diventare st_union. Ecco perché sembra un po 'divertente
LR1234567,

Risposte:


19

Quindi, per esempio. Ecco una semplice tabella con due gruppi di bordi collegati:

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

Ora, ecco una funzione ricorsiva che, dato l'id di un bordo, accumula tutti i bordi che toccano:

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

Questo ci lascia solo bisogno di trovare, dopo che ogni gruppo è stato accumulato, l'id di un bordo che non fa già parte di un gruppo. Che, tragicamente, richiede una seconda query ricorsiva.

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;   

Che presi insieme restituiscono un bel set con l'id seme e ogni gruppo che ha accumulato. Lascio che sia un esercizio per il lettore trasformare le matrici di id in una query per creare geometrie per la mappatura.

 id |   grouplist   
----+---------------
  1 | {1,2,3,4}
 11 | {11,12,13,14}
(2 rows)

Penso che questo codice potrebbe essere più semplice, se il tipo di geometria supporta l'hashing in PostgreSQL (quando si scrive un RCTE più semplice che non comporta l'accumulo di matrici di ID, si ottiene un errore "Tutti i tipi di dati di colonna devono essere hash"), quindi c'è un piccola richiesta di miglioramento per me.
Paul Ramsey,

Questo è un approccio davvero fantastico. Sto notando alcuni risultati strani mentre lo applico a un set di test più ampio; Vedrò se posso ridurre il problema a un semplice esempio. 100 righe: 85 cluster, cluster più grande = 3, 0,03 s //// 200 righe: 144 cluster, cluster più grande = 9, 0,08 s //// 300 righe: 180 cluster, cluster più grande = 51, 0,16 s /// / 400 righe: 188 cluster, cluster più grande = 41, 0,27 s //// 500 righe: 176 cluster, cluster più grande = 112, 0,56 s //// 600 righe: 143 cluster, cluster più grande = 449, 1.0 s // // 650 righe: 133 cluster, cluster più grande = 7601, 6,8 s
dbaston,

Aggiungendo questo al dati di test causerà ID duplicati nella grouplistmatrice: insert into lines (id, geom) values ( 15, 'LINESTRING(0 0, 10 10)');. La modifica array_agg(id)della funzione torna a array_agg(DISTINCT id)sembra risolvere il problema.
dbaston,

Questa è una buona soluzione, quindi ora come possiamo memorizzare le geometrie in una tabella in modo da poter vedere le linee collegate?
zakaria mouqcit,

6

Ecco un approccio che utilizza una tabella temporanea per aggregare in modo incrementale i cluster insieme. Non mi interessa molto l'approccio della tabella temporanea, ma questo sembra funzionare abbastanza bene all'aumentare del numero di linee (ho 1,2 M linee nel mio input).

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;

funziona perfettamente
zakaria mouqcit,

@zakariamouqcit Sono contento che questo abbia funzionato per te! Ho scritto questa risposta prima di scrivere la ST_ClusterIntersectingfunzione in PostGIS. Se i tuoi dati sono abbastanza piccoli da adattarsi alla memoria, ti suggerirei di verificarli per una soluzione più performante.
dbaston,

la ricerca di questa domanda mi ha portato qui. Ho provato l'iterativo e lo st_clusterintersecting ma ho trovato lo st_clusterDBScan il più adatto. Nel caso in cui anche qualcun altro venga portato qui. postgis.net/docs/manual-dev/ST_ClusterDBSCAN.html
D_C

D'accordo, ST_ClusterDBSCAN è quasi sempre il modo migliore per PostGIS
2.3+
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.