Группировка связанных строк в PostGIS?

12

У меня есть таблица улиц, которую я выбрал на основе набора атрибутов (скажем, так speed_limit < 25). Есть группы улиц, которые являются местными смежными; Я хотел бы сгруппировать эти наборы связанных строк в GeometryCollections. На изображении ниже будет две GeometryCollections: одна с красными линиями и одна с синими линиями.

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

Я попытался выполнить несколько запросов «распустить, дезагрегировать» по следующим направлениям:

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

Со всем, что я пробовал, я либо получаю одну особенность ( ST_Union) или мою исходную геометрию ( ST_Dumpиз ST_Union).

Может быть, это можно сделать с помощью какой-то WITH RECURSIVEмагии?

dbaston
источник
Что-то не так с "(ST_Dump (st_union)). Geom"
Martin F
Поскольку он не использовал псевдоним ST_Union (geom), имя нового 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 |   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 с
dbaston
Добавление этого в тестовых данных будет вызывать повторяющиеся идентификаторы в grouplistмассиве: insert into lines (id, geom) values ( 15, 'LINESTRING(0 0, 10 10)');. Изменение array_agg(id)в функции возврата к, array_agg(DISTINCT id)кажется, решает проблему.
dbaston
Это хорошее решение, так что теперь, как мы можем сохранить геометрию в таблице, чтобы мы могли видеть связанные линии?
Закария Моккит
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;
dbaston
источник
отлично работает
Zakaria Mouqcit
@zakariamouqcit Рад, что это сработало для вас! Я написал этот ответ до того, как написал ST_ClusterIntersectingфункцию в PostGIS. Если ваши данные достаточно малы, чтобы поместиться в памяти, я бы посоветовал проверить это для более производительного решения.
17
поиск этого вопроса привел меня сюда. Попробовал итерацию и st_clusterintersecting, но обнаружил, что st_clusterDBScan является наиболее подходящим. В случае, если кто-то еще приведен сюда. postgis.net/docs/manual-dev/ST_ClusterDBSCAN.html
D_C
Согласен, ST_ClusterDBSCAN - почти всегда лучший путь для PostGIS 2.3+
dbaston