Групування підключених рядків у PostGIS?


12

У мене є таблиця вулиць, яку я вибрав на основі набору атрибутів (скажімо так speed_limit < 25). Є групи вулиць, які локально суміжні; Я хотів би згрупувати ці набори з'єднаних рядків у GeometryCollections. На зображенні нижче було б два геометричні колекції: один з червоними лініями та один із синіми лініями.

введіть тут опис зображення

Я спробував виконати пару запитів "розчинення, деагрегатування" за рядками:

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

З усього, що я спробував, я або закінчуюсь однією функцією ( ST_Union), або своєю оригінальною геометрією ( ST_Dumpз ST_Union).

Може, це можливо зробити якоюсь WITH RECURSIVEмагією?


Щось не виглядає правильно з "(ST_Dump (st_union)). Geom"
Martin F

Оскільки він не отримав псевдонім ST_Union (geom), ім'я нового геома успадкувало ім'я функції стати st_union. Ось чому це виглядає трохи смішно
LR1234567

Відповіді:


19

Так, на прикладі. Ось проста таблиця з двома пов'язаними групами ребер:

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

Тепер ось рекурсивна функція, яка з урахуванням ідентифікатора ребра акумулює всі ребра, які торкаються:

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

Це просто залишає нам потребу знайти після накопичення кожної групи ідентифікатор краю, який вже не є частиною групи. Що трагічно потребує другого рекурсивного запиту.

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;   

Що взяли разом, повертають гарний набір з насіннєвим ідентифікатором та кожною групою, яку він накопичив. Я залишаю це як вправу для читача, щоб повернути масиви id назад у запит, щоб створити геометрію для відображення.

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

Я думаю, що цей код може бути простішим, якщо тип геометрії, що підтримує хешування в PostgreSQL (коли ви пишете простіший RCTE, який не передбачає накопичення масивів ідентифікаторів, ви отримуєте помилку "Усі типи даних стовпців повинні бути доступними"), тому невеликий запит на покращення для мене.
Пол Рамзі

Це дійсно приголомшливий підхід. Я помічаю деякі дивні результати, коли застосовую його до більшого тестового набору; Я побачу, чи зможу я звести проблему до простого прикладу. 100 рядків: 85 кластерів, найбільший кластер = 3, 0,03 с //// 200 рядків: 144 кластери, найбільший кластер = 9, 0,08 с //// 300 рядків: 180 кластерів, найбільший кластер = 51, 0,16 с /// / 400 рядків: 188 кластерів, найбільший кластер = 41, 0,27 с //// 500 рядків: 176 кластерів, найбільший кластер = 112, 0,56 с //// 600 рядків: 143 кластери, найбільший кластер = 449, 1,0 с // // 650 рядків: 133 кластери, найбільший кластер = 7601, 6,8 с
дбастон

Додавання цього в тестових даних буде викликати повторювані ідентифікатори в grouplistмасиві: insert into lines (id, geom) values ( 15, 'LINESTRING(0 0, 10 10)');. Зміна array_agg(id)повернення функції на, array_agg(DISTINCT id)здається, вирішує проблему.
дбастон

Це хороше рішення, тож як тепер ми можемо зберігати геометрії в таблиці, щоб ми могли бачити пов'язані лінії?
zakaria mouqcit

6

Ось підхід, який використовує тимчасову таблицю для поступового агрегування кластерів разом. Мене не дуже цікавить підхід до тимчасової таблиці, але це, здається, працює досить добре, оскільки кількість рядків збільшується (у мене є 1,2 М рядків).

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;

працює чудово
zakaria mouqcit

@zakariamouqcit Рада, що це працювало для вас! Цю відповідь я написав ще до того, як ST_ClusterIntersectingфункцію написав у PostGIS. Якщо ваші дані досить малі, щоб вміститись у пам’яті, я б радив перевірити це на більш ефективне рішення.
дбастон

пошук цього питання привів мене сюди. Спробував ітеративний і st_clusterinterseting, але виявив, що st_clusterDBScan є найбільш підходящим. На випадок, коли сюди приведуть ще когось. postgis.net/docs/manual-dev/ST_ClusterDBSCAN.html
D_C

Погоджено, ST_ClusterDBSCAN - це майже завжди найкращий спосіб перейти до PostGIS 2.3+
dbaston
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.