Mengelompokkan linestrings yang terhubung di PostGIS?

12

Saya memiliki tabel jalan yang saya pilih berdasarkan seperangkat atribut (katakanlah itu speed_limit < 25). Ada kelompok jalan yang berdekatan secara lokal; Saya ingin mengelompokkan kumpulan linestrings yang terhubung ini ke GeometryCollections. Pada gambar di bawah ini, akan ada dua GeometryCollections: satu dengan garis merah dan satu dengan garis biru.

masukkan deskripsi gambar di sini

Saya mencoba menjalankan beberapa kueri "bubar, lepas landas" di sepanjang baris:

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

Dengan semua yang telah saya coba, saya berakhir dengan fitur tunggal ( ST_Union) atau geometri asli saya ( ST_Dumpof ST_Union).

Mungkin ini mungkin dilakukan dengan semacam WITH RECURSIVEsihir?

dbaston
sumber
Sesuatu tidak beres dengan "(ST_Dump (st_union)). Geom"
Martin F
Karena dia tidak alias ST_Union (geom) nama geom baru mewarisi nama fungsi untuk menjadi st_union. Itu sebabnya terlihat sedikit lucu
LR1234567

Jawaban:

19

Jadi, dengan contoh. Berikut adalah tabel sederhana dengan dua kelompok tepi yang terhubung:

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

Sekarang, inilah fungsi rekursif yang, mengingat id tepi, mengakumulasikan semua tepi yang menyentuh:

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

Itu hanya membuat kita perlu menemukan, setelah setiap kelompok diakumulasikan, id dari edge yang belum menjadi bagian dari grup. Yang, tragisnya, membutuhkan permintaan rekursif kedua.

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;   

Yang diambil bersama-sama mengembalikan satu set yang bagus dengan id benih dan masing-masing kelompok akumulasi. Saya meninggalkannya sebagai latihan untuk pembaca untuk mengubah array id kembali menjadi permintaan untuk membuat geometri untuk pemetaan.

 id |   grouplist   
----+---------------
  1 | {1,2,3,4}
 11 | {11,12,13,14}
(2 rows)
Paul Ramsey
sumber
Saya pikir kode ini bisa lebih sederhana, jika tipe geometri yang mendukung hashing di PostgreSQL (ketika Anda menulis RCTE yang lebih sederhana yang tidak melibatkan akumulasi array id, Anda mendapatkan kesalahan "Semua tipe data kolom harus hashable"), jadi ada sedikit permintaan tambahan untuk saya.
Paul Ramsey
Ini adalah pendekatan yang sangat mengagumkan. Saya memperhatikan beberapa hasil aneh saat saya menerapkannya pada set tes yang lebih besar; Saya akan melihat apakah saya dapat mengurangi masalah menjadi contoh sederhana. 100 baris: 85 kluster, klaster terbesar = 3, 0,03 s //// 200 baris: 144 klaster, klaster terbesar = 9, 0,08 s //// 300 baris: 180 klaster, klaster terbesar = 51, 0,16 s /// / 400 baris: 188 kluster, klaster terbesar = 41, 0,27 s //// 500 baris: 176 klaster, klaster terbesar = 112, 0,56 s //// 600 baris: 143 klaster, klaster terbesar = 449, 1,0 s // // 650 baris: 133 cluster, cluster terbesar = 7601, 6,8 s
dbaston
Menambahkan ini ke data pengujian akan menyebabkan duplikat ID dalam grouplistberbagai: insert into lines (id, geom) values ( 15, 'LINESTRING(0 0, 10 10)');. Mengubah array_agg(id)fungsi kembali ke array_agg(DISTINCT id)tampaknya menyelesaikan masalah.
dbaston
Ini adalah solusi yang baik, jadi sekarang bagaimana kita bisa menyimpan geometri dalam sebuah tabel sehingga kita dapat melihat garis yang terhubung?
zakaria mouqcit
6

Berikut adalah pendekatan yang menggunakan tabel sementara untuk secara bertahap mengumpulkan kelompok bersama-sama. Saya tidak terlalu peduli dengan pendekatan tabel sementara, tetapi ini tampaknya berkinerja cukup baik karena jumlah baris meningkat (saya memiliki 1,2 baris M dalam input saya).

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
sumber
bekerja dengan sempurna
zakaria mouqcit
@zakariamouqcit Senang ini berhasil untuk Anda! Saya menulis jawaban ini sebelum saya menulis ST_ClusterIntersectingfungsinya di PostGIS. Jika data Anda cukup kecil untuk masuk ke dalam memori, saya sarankan memeriksa itu untuk solusi yang lebih berkinerja.
dbaston
mencari pertanyaan ini membawaku ke sini. Mencoba iteratif dan st_clusterintersecting tetapi menemukan st_clusterDBScan sebagai yang paling tepat. Kalau-kalau ada orang lain yang dibawa ke sini juga. postgis.net/docs/manual-dev/ST_ClusterDBSCAN.html
D_C
Setuju, ST_ClusterDBSCAN hampir selalu merupakan cara terbaik untuk menggunakan PostGIS 2.3+
dbaston