Dividi le linee in sottoinsiemi non sovrapposti in base ai punti


10

Data una tabella con geometria della linea e uno o più punti che vengono agganciati a questa linea in una tabella separata, vorrei dividere ogni linea con uno o più punti che si intersecano in ciascuna delle posizioni in cui la linea interseca un punto.

Ad esempio, c'è una linea, L, con tre punti di intersezione, A, B e C in ordine lungo la geometria della linea. Vorrei restituire L come quattro geometrie distinte: dall'inizio di L ad A, da A a B lungo L, da B a C lungo L e da C fino alla fine di L.

In passato ho usato ben fatto per questo compito, che è un problema di riferimento lineare ( http://sgillies.net/blog/1040/shapely-recipes/ ). Tuttavia, questo non sarebbe praticabile in questo caso, che ha molti milioni di linee e punti. Invece, sto cercando una soluzione usando PostgreSQL / PostGIS.

Nota che i punti sono vincolati ad essere su una linea. Inoltre, un punto può essere validamente all'inizio o alla fine di una linea, nel qual caso la linea non deve essere divisa (a meno che non vi siano altri punti che non coincidono con i punti iniziale o finale della stessa linea). Le linee del sottoinsieme devono conservare la loro direzione e i loro attributi, ma gli attributi delle caratteristiche dei punti non contano.

Risposte:


7

La funzione PostGIS ST_Split è probabilmente ciò che desideri.

PostGIS 2.2+ ora supporta le geometrie Multi * in ST_Split.

Per le versioni precedenti di PostGIS, continua a leggere:


Per ottenere una singola linea divisa per più punti, è possibile utilizzare qualcosa come questa funzione plpgsql del wrapper multipunto . L'ho semplificato solo al caso "diviso (multi) linee con (multi) punti" di seguito:

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;

Quindi per creare una geometria multipunto da tagliare, utilizzare ST_Collect e crearla manualmente dagli input:

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

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

O aggregalo da una sottoquery:

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

Ho provato ST_Split per cominciare e sono rimasto sorpreso quando ho scoperto che non accettava la geometria multipunto. La tua funzione sembra colmare quel vuoto, ma sfortunatamente sta restituendo NULL per il caso multipunto di esempio. (Funziona bene sul (singolo) punto.) Tuttavia, ho cambiato SE blade_geometry_type NON ILIKE '% LINESTRING' THEN a IF blade_geometry_type ILIKE '% LINESTRING' THEN nella tua funzione e ho ottenuto il risultato atteso e corretto `GEOMETRYCOLLECTION '. Tuttavia, sono ancora abbastanza nuovo su PostGIS, quindi questa modifica è sensata?
alphabetasoup,

Scusa, avrei dovuto essere IF geom_geometry_type NOT ILIKE '%LINESTRING' THEN- l'ho modificato.
rcoup

1
Ah, capisco. Grazie, questa è un'ottima soluzione. Dovresti suggerire questo come contributo a ST_Split in modo che possa gestire multilinea e multipunto, se questo non è già nella pipeline PostGIS.
alphabetasoup

3
ST_Splitsupporta multi * blade dentro postgis 2.2e sopra postgis.net/docs/ST_Split.html
raphael

3

Esegui l'upgrade a PostGIS 2.2 , dove ST_Split è stato espanso per supportare la divisione per un confine poligonale, multiplo o multiplo.

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

È brillante.
alphabetasoup,

Questo non funziona per il mio complicato geom: gist.github.com/ideamotor/7bd7cdee15f410ce12f3aa14ebf70177
ideamotor

funziona con ST_Snap, ala trac.osgeo.org/postgis/ticket/2192
ideamotor

2

Non ho la risposta completa per te, ma ST_Line_Locate_Point prende una linea e un punto come argomenti e restituisce un numero compreso tra 0 e 1 che rappresenta la distanza lungo la linea nella posizione più vicina al punto.

ST_Line_Substring accetta una riga e due numeri, ciascuno compreso tra 0 e 1, come argomenti. I numeri rappresentano le posizioni sulla linea come distanze frazionarie. La funzione restituisce il segmento di linea che corre tra quelle due posizioni.

Lavorando con queste due funzioni, dovresti essere in grado di ottenere ciò che vuoi fare.


Grazie per questo. In realtà ho risolto questo problema usando la tua tecnica e quella di @rcoup. Gli ho dato la risposta accettata a causa della funzione che dovrebbe facilitare gli altri. Se altri vogliono seguire questo percorso, ho creato una tabella temporanea delle linee che hanno punti su di esse, con una riga per ogni linea e uno stop su di essa. Ho aggiunto colonne per l'output di ST_Line_Locate_Point (line.geom, pt.geom) AS L e una funzione finestra: rank () OVER PARTITION BY line.id ORDER BY LR). Quindi SINISTRA ESTERNO UNISCITI alla tabella temporanea, a, a se stessa, b, dove a.id = b.id e a.LR = b.LR + 1 (continua)
alphabetasoup

(continua) Il join esterno consente un CASO QUANDO i campi di join sono nulli, nel qual caso ST_Line_Substring dal punto alla fine della linea, altrimenti ST_Line_Substring dal riferimento lineare del primo punto, al riferimento lineare del secondo punto (con rango superiore). Ottenere il segmento [start] LA viene quindi eseguito con un secondo SELECT, semplicemente selezionando quelli con un rango di 1 e calcolando lo ST_Line_Substring dal punto ST_Start della linea al riferimento lineare del punto di intersezione. Inseriscili nel tavolo, ricordando di mantenere line.id e voilà. Saluti.
alphabetasoup

Puoi pubblicare questa risposta come risposta nel codice per favore? Mi piacerebbe guardare quell'opzione e sono un po 'un principiante di SQL.
Phil Donovan,

1
@PhilDonovan: fatto.
alphabetasoup,

2

Mi è stato chiesto questo due volte, quindi mi dispiace per il ritardo. È improbabile che ciò sia considerato una soluzione concisa; L'ho scritto un po 'più in basso nella curva di apprendimento di quanto lo sia attualmente. Qualche consiglio benvenuto, anche stilistico.

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

Voglio espandere le risposte sopra dal punto di vista di un principiante. In questo scenario, hai una serie di punti e guardi come usarli come una "lama" per tagliare una linea in segmenti. L'intero esempio presuppone che i punti siano stati prima agganciati alla linea e che i punti abbiano l'attributo ID univoco dalla loro linea spezzata. Uso "column_id" per rappresentare l'ID univoco della riga.

Innanzitutto , vuoi raggruppare i tuoi punti in punti multipli quando più di una lama cade su una linea. Altrimenti, la funzione split_line_multipoint si comporta come la funzione ST_Split, che non è il risultato desiderato.

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;

Quindi , si desidera dividere la rete in base a questi punti multipli.

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;


Ripeti i passaggi 1 e 2 con le linee che hanno solo un punto di intersezione. Per fare ciò, è necessario aggiornare il codice dal passaggio 1 a "HAVING COUNT (*) = 1". Rinominare le tabelle di conseguenza.


Quindi , crea una tabella di linee duplicate ed elimina le voci con punti su di esse.

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


Infine , unisciti ai tuoi tre tavoli insieme usando 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;

BAM!

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.