Розділіть рядки на підмножини, що не перекриваються, на основі точок


10

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

Наприклад, існує лінія L з трьома точками, що перетинаються, A, B і C у порядку вздовж геометрії лінії. Я хотів би повернути L як чотири окремі геометрії: від початку L до A, від A до B вздовж L, від B до C уздовж L і від C до кінця L.

Раніше я часто використовував для цього завдання, яке є лінійною проблемою посилання ( http://sgillies.net/blog/1040/shapely-recipes/ ). Однак це не було б можливо в цьому випадку, який має багато мільйонів рядків і точок. Натомість я шукаю рішення за допомогою PostgreSQL / PostGIS.

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

Відповіді:


7

Функція ST_Split PostGIS - це, мабуть, те, що ви хочете.

PostGIS 2.2+ тепер підтримує Multi * геометрії в ST_Split.

Старіші версії PostGIS читайте на:


Щоб розділити один рядок на кілька точок, ви можете використовувати щось на кшталт цієї багатопоточної обгортки plpgsql. Я спростив це лише до випадку "розділити (кілька) рядків з (декількома) точками" нижче:

DROP FUNCTION IF EXISTS split_line_multipoint(input_geom geometry, blade geometry);
CREATE FUNCTION split_line_multipoint(input_geom geometry, blade geometry)
  RETURNS geometry AS
$BODY$
    -- this function is a wrapper around the function ST_Split 
    -- to allow splitting multilines with multipoints
    --
    DECLARE
        result geometry;
        simple_blade geometry;
        blade_geometry_type text := GeometryType(blade);
        geom_geometry_type text := GeometryType(input_geom);
    BEGIN
        IF blade_geometry_type NOT ILIKE 'MULTI%' THEN
            RETURN ST_Split(input_geom, blade);
        ELSIF blade_geometry_type NOT ILIKE '%POINT' THEN
            RAISE NOTICE 'Need a Point/MultiPoint blade';
            RETURN NULL;
        END IF;

        IF geom_geometry_type NOT ILIKE '%LINESTRING' THEN
            RAISE NOTICE 'Need a LineString/MultiLineString input_geom';
            RETURN NULL;
        END IF;

        result := input_geom;           
        -- Loop on all the points in the blade
        FOR simple_blade IN SELECT (ST_Dump(ST_CollectionExtract(blade, 1))).geom
        LOOP
            -- keep splitting the previous result
            result := ST_CollectionExtract(ST_Split(result, simple_blade), 2);
        END LOOP;
        RETURN result;
    END;
$BODY$
LANGUAGE plpgsql IMMUTABLE;

-- testing
SELECT ST_AsText(split_line_multipoint(geom, blade))
    FROM (
        SELECT ST_GeomFromText('Multilinestring((-3 0, 3 0),(-1 0, 1 0))') AS geom,
        ST_GeomFromText('MULTIPOINT((-0.5 0),(0.5 0))') AS blade
        --ST_GeomFromText('POINT(-0.5 0)') AS blade
    ) AS T;

Потім, щоб створити багатоточкову геометрію, яку слід вирізати, використовуйте ST_Collect і будь-коли створюйте її вручну з входів:

SELECT ST_AsText(ST_Collect(
  ST_GeomFromText('POINT(1 2)'),
  ST_GeomFromText('POINT(-2 3)')
));

st_astext
----------
MULTIPOINT(1 2,-2 3)

Або зібрати його з підзапиту:

SELECT stusps,
  ST_Multi(ST_Collect(f.the_geom)) as singlegeom
FROM (SELECT stusps, (ST_Dump(the_geom)).geom As the_geom
      FROM somestatetable ) As f
GROUP BY stusps

Я спробував ST_Split для початку і здивувався, коли виявив, що він не приймає багатоточкову геометрію. Здається, ваша функція заповнює цей пробіл, але, на жаль, він повертає NULL для прикладу багатоточкового випадку. (Це добре працює в (єдиній) точці.) Однак я змінив IF blade_geometry_type NOT ILIKE '% LINESTRING THEN to IF blade_geometry_type ILIKE'% LINESTRING 'THEN у вашій функції та отримав очікуваний та правильний результат' GEOMETRYCOLLECTION '. Я все ще досить новачок у PostGIS, але чи є така модифікація розумною?
alphabetasoup

Вибачте, мав би бути IF geom_geometry_type NOT ILIKE '%LINESTRING' THEN- я це відредагував.
rcoup

1
А, бачу. Дякую, це чудове рішення. Ви повинні запропонувати це як внесок у ST_Split, щоб він міг обробляти багаторядкові та багатоточкові, якщо цього ще немає в конвеєрі PostGIS.
alphabetasoup

3
ST_Splitпідтримує мультитач * Лопаті в postgis 2.2і вище postgis.net/docs/ST_Split.html
Raphael

3

Оновіть до PostGIS 2.2 , де ST_Split було розширено для підтримки розщеплення на багатолінійну, багатоточну або (багато) багатокутну межу.

postgis=# SELECT postgis_version(),
                  ST_AsText(ST_Split('LINESTRING(0 0, 2 0)', 'MULTIPOINT(0 0, 1 0)'));
-[ RECORD 1 ]---+------------------------------------------------------------
postgis_version | 2.2 USE_GEOS=1 USE_PROJ=1 USE_STATS=1
st_astext       | GEOMETRYCOLLECTION(LINESTRING(1 0,2 0),LINESTRING(0 0,1 0))

Це геніально.
alphabetasoup

Це не працює для мого складного geom: gist.github.com/ideamotor/7bd7cdee15f410ce12f3aa14ebf70177
ideamotor

вона працює з ST_Snap, ala trac.osgeo.org/postgis/ticket/2192
ideamotor

2

Я не відповідав за вас цілком, але ST_Line_Locate_Point бере аргумент рядка і крапки і повертає число між 0 і 1, що представляє відстань уздовж рядка, до позиції, найближчої до точки.

ST_Line_Substring бере аргументи рядка та двох чисел, кожне від 0 до 1. Цифри представляють позиції на лінії у вигляді дробових відстаней. Функція повертає відрізок рядка, який проходить між цими двома позиціями.

Працюючи з цими двома функціями, ви повинні мати можливість досягти того, що хочете зробити.


Дякую за це Я фактично вирішив цю проблему, використовуючи вашу техніку, а також проблему від @rcoup. Я дав йому прийняту відповідь завдяки функції, яка повинна полегшити для інших. Якщо інші хочуть піти цим шляхом, я створив тимчасову таблицю ліній, на яких є точки, із рядком для кожного рядка та однією зупинкою, яка знаходиться на ньому. Я додав стовпці для виводу ST_Line_Locate_Point (line.geom, pt.geom) AS L та віконну функцію: rank () ЗА ПАРТІЄНЮ ПО line.id ORDER BY LR). Тоді ВЛІВО ВІД'ЄДНУЙТЕСЬ приєднуйтесь до тимчасової таблиці, a, до себе, b, де a.id = b.id і a.LR = b.LR + 1 (продовження)
alphabetasoup

(продовження) Зовнішнє з'єднання дає можливість СЛУЧАЙ, коли поля приєднання є нульовими; в цьому випадку ST_Line_Substring від точки до кінця рядка, інакше ST_Line_Substring від лінійної посилання першої точки, до лінійної посилання другої точки (з вищим званням). Отримання сегменту [start] LA виконується за допомогою другого SELECT, просто вибираючи ті, хто має ранг 1, і обчислює ST_Line_Substring від лінії ST_StartPoint лінії до лінійного відліку точки перетину. Помістіть їх у таблиці, пам’ятаючи про збереження рядків.id та voilà. Ура.
alphabetasoup

Чи можете ви опублікувати цю відповідь як відповідь у коді, будь ласка? Я хотів би розглянути цей варіант, а також я трохи новачок у SQL.
Філ Донован

1
@PhilDonovan: зроблено.
alphabetasoup

2

Мене про це просили вже двічі, тож шкода за затримку. Це навряд чи можна вважати стислим рішенням; Я написав це, коли трохи далі за кривою навчання, ніж я зараз. Будь-які поради вітаються, навіть стилістичні.

--Inputs:
--walkingNetwork = Line features representing edges pedestrians can walk on
--stops = Bus stops
--NOTE: stops.geom is already constrained to be coincident with line features
--from walkingNetwork. They may be on a vertex or between two vertices.

--This series of queries returns a version of walkingNetwork, with edges split
--into separate features where they intersect stops.

CREATE TABLE tmp_lineswithstops AS (
    WITH subq AS (
        SELECT
        ST_Line_Locate_Point(
            roads.geom,
            ST_ClosestPoint(roads.geom, stops.geom)
        ) AS LR,
        rank() OVER (
            PARTITION BY roads.gid
            ORDER BY ST_Line_Locate_Point(
                roads.geom,
                ST_ClosestPoint(roads.geom, stops.geom)
            )
        ) AS LRRank,
        ST_ClosestPoint(roads.geom, stops.geom),
        roads.*
        FROM walkingNetwork AS roads
        LEFT OUTER JOIN stops
        ON ST_Distance(roads.geom, stops.geom) < 0.01
        WHERE ST_Equals(ST_StartPoint(roads.geom), stops.geom) IS false
        AND ST_Equals(ST_EndPoint(roads.geom), stops.geom) IS false
        ORDER BY gid, LRRank
    )
    SELECT * FROM subq
);

-- Calculate the interior edges with a join
--If the match is null, calculate the line to the end
CREATE TABLE tmp_testsplit AS (
    SELECT
    l1.gid,
    l1.geom,
    l1.lr AS LR1,
    l1.st_closestpoint AS LR1geom,
    l1.lrrank AS lr1rank,
    l2.lr AS LR2,
    l2.st_closestpoint AS LR2geom,
    l2.lrrank AS lr2rank,
    CASE WHEN l2.lrrank IS NULL -- When the point is the last along the line
        THEN ST_Line_Substring(l1.geom, l1.lr, 1) --get the substring line to the end
        ELSE ST_Line_Substring(l1.geom, l1.lr, l2.lr) --get the substring between the two points
    END AS sublinegeom
    FROM tmp_lineswithstops AS l1
    LEFT OUTER JOIN tmp_lineswithstops AS l2
    ON l1.gid = l2.gid
    AND l2.lrrank = (l1.lrrank + 1)
);

--Calculate the start to first stop edge
INSERT INTO tmp_testsplit (gid, geom, lr1, lr1geom, lr1rank, lr2, lr2geom, lr2rank, sublinegeom)
SELECT gid, geom,
0 as lr1,
ST_StartPoint(geom) as lr1geom,
0 as lr1rank,
lr AS lr2,
st_closestpoint AS lr2geom,
lrrank AS lr2rank,
ST_Line_Substring(l1.geom, 0, lr) AS sublinegeom --Start to point
FROM tmp_lineswithstops AS l1
WHERE l1.lrrank = 1;

--Now match back to the original road features, both modified and unmodified
CREATE TABLE walkingNetwork_split AS (
    SELECT
    roadssplit.sublinegeom,
    roadssplit.gid AS sgid, --split-gid
    roads.*
    FROM tmp_testsplit AS roadssplit
    JOIN walkingNetwork AS r
    ON r.gid = roadssplit.gid
    RIGHT OUTER JOIN walkingNetwork AS roads --Original edges with null if unchanged, original edges with split geom otherwise
    ON roads.gid = roadssplit.gid
);

--Now update the necessary columns, and drop the temporary columns
--You'll probably need to work on your own length and cost functions
--Here I assume it's valid to just multiply the old cost by the fraction of
--the length the now-split line represents of the non-split line
UPDATE walkingNetwork_split
SET geom = sublinegeom,
lengthz = lengthz*(ST_Length(sublinegeom)/ST_Length(geom)),
walk_seconds_ft = walk_seconds_ft*(ST_Length(sublinegeom)/ST_Length(geom)),
walk_seconds_tf = walk_seconds_tf*(ST_Length(sublinegeom)/ST_Length(geom))
WHERE sublinegeom IS NOT NULL
AND ST_Length(sublinegeom) > 0;
ALTER TABLE walkingNetwork_split
DROP COLUMN sublinegeom,
DROP COLUMN sgid;

--Drop intermediate tables
--You probably could use actual temporary tables;
--I prefer to have a sanity check at each stage
DROP TABLE IF EXISTS tmp_testsplit;
DROP TABLE IF EXISTS tmp_lineswithstops;

--Assign the edges a new unique id, so we can use this as source/target columns in pgRouting
ALTER TABLE walkingNetwork_split
DROP COLUMN IF EXISTS fid;
ALTER TABLE walkingNetwork_split
ADD COLUMN fid INTEGER;
CREATE SEQUENCE roads_seq;
UPDATE walkingNetwork_split
SET fid = nextval('roads_seq');
ALTER TABLE walkingNetwork_split
ADD PRIMARY KEY ("fid");

0

Я хочу розкрити відповіді вище з точки зору початківця. У цьому сценарії у вас є ряд точок, і ви спостерігаєте, як використовувати їх як «лезо», щоб вирізати лінії на сегменти. Весь цей приклад передбачає, що ви спочатку прив’язали свої очки до рядка і що точки мають унікальний атрибут ідентифікатора з їх відрізаного рядка. Я використовую "column_id" для представлення унікального ідентифікатора рядка.

По-перше , ви хочете згрупувати свої точки в багатоточкові точки, коли більше однієї лезо падає на лінію. В іншому випадку функція split_line_multipoint діє як функція ST_Split, що не є бажаним результатом.

CREATE TABLE multple_terminal_lines AS
SELECT ST_Multi(ST_Union(the_geom)) as the_geom, a.matched_alid
FROM    point_table a
        INNER JOIN
        (
            SELECT  column_id
            FROM    point_table
            GROUP   BY column_id
            HAVING  COUNT(*) > 1
        ) b ON a.column_id = b.column_id
GROUP BY a.column_id;

Потім ви хочете розділити свою мережу на основі цих багатоточок.

CREATE TABLE split_multi AS
SELECT (ST_Dump(split_line_multipoint(ST_Snap(a.the_geometry, b.the_geom, 0.00001),b.the_geom))).geom as the_geom
FROM line_table a
JOIN multple_terminal_lines b 
ON a.column_id = b.column_id;


Повторіть кроки 1 і 2 зі своїми лініями, які мають лише одну точку перетину. Для цього вам слід оновити код з кроку 1 до "HAVING COUNT (*) = 1". Перейменуйте таблиці відповідно.


Далі складіть таблицю рядків, що повторюється, і видаліть записи з крапками на них.

CREATE TABLE line_dup AS
SELECT * FROM line_table;
-- Delete shared entries
DELETE FROM line_dup
WHERE column_id in (SELECT DISTINCT column_id FROM split_single) OR column_id in (SELECT DISTINCT column_id FROM split_multi) ;


Нарешті , з'єднайте свої три таблиці разом, використовуючи UNION ALL:

CREATE TABLE line_filtered AS 
SELECT the_geom
FROM split_single
UNION ALL 
SELECT the_geom
FROM split_multi
UNION ALL 
SELECT the_geom
FROM line_dup;

БАМ!

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