Dividir líneas en subconjuntos no superpuestos basados ​​en puntos

10

Dada una tabla con geometría de línea, y uno o más puntos que se ajustan a esta línea en una tabla separada, me gustaría dividir cada línea con uno o más puntos de intersección en cada una de las ubicaciones donde la línea intersecta un punto.

Por ejemplo, hay una línea, L, con tres puntos de intersección, A, B y C en orden a lo largo de la geometría de la línea. Me gustaría devolver L como cuatro geometrías distintas: desde el comienzo de L a A, de A a B a lo largo de L, de B a C a lo largo de L, y de C al final de L.

En el pasado, he utilizado bien para esta tarea, que es un problema de referencia lineal ( http://sgillies.net/blog/1040/shapely-recipes/ ). Sin embargo, esto no sería factible en este caso, que tiene muchos millones de líneas y puntos. En cambio, estoy buscando una solución usando PostgreSQL / PostGIS.

Tenga en cuenta que los puntos están restringidos a estar en una línea. Además, un punto puede estar válidamente en el inicio o el final de una línea, en cuyo caso la línea no necesita dividirse (a menos que haya otros puntos que no coincidan con los puntos de inicio o finalización de la misma línea). Las líneas de subconjunto necesitan conservar su dirección y sus atributos, pero los atributos de las entidades de puntos no importan.

alphabetasoup
fuente

Respuestas:

7

La función ST_Split PostGIS es probablemente lo que desea.

PostGIS 2.2+ ahora admite geometrías Multi * en ST_Split.

Para versiones anteriores de PostGIS, siga leyendo:


Para obtener una sola línea dividida por varios puntos, puede usar algo como esta función plpgsql de envoltura multipunto . Lo simplifiqué solo con el caso de "líneas divididas (múltiples) con (múltiples) puntos a continuación:

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;

Luego, para crear una geometría multipunto para cortar, use ST_Collect y créelo manualmente desde las entradas:

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

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

O agregarlo desde una subconsulta:

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
rcoup
fuente
Al principio intenté con ST_Split, y me sorprendió cuando descubrí que no aceptaba geometría multipunto. Su función parece llenar ese vacío, pero desafortunadamente está devolviendo NULL para el caso multipunto de ejemplo. (Funciona bien en el punto (único)). Sin embargo, cambié IF blade_geometry_type NOT ILIKE '% LINESTRING' THEN a IF blade_geometry_type ILIKE '% LINESTRING' THEN en su función y obtuve el resultado esperado y correcto 'GEOMETRYCOLLECTION'. Sin embargo, todavía soy bastante nuevo en PostGIS, entonces, ¿es razonable esa modificación?
alphabetasoup
Lo siento, debería haberlo sido IF geom_geometry_type NOT ILIKE '%LINESTRING' THEN. Lo he editado.
rcoup
1
Ah, ya veo. Gracias, esta es una gran solución. Debe sugerir esto como una contribución a ST_Split para que pueda manejar líneas múltiples y multipunto, si esto aún no está en la tubería de PostGIS.
alphabetasoup
3
ST_Splitadmite múltiples * cuchillas en postgis.net/docs/ST_Split.htmlpostgis 2.2 y posteriores
raphael
3

Actualice a PostGIS 2.2 , donde ST_Split se expandió para admitir la división por un límite multilínea, multipunto o (multi) polígono.

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))
Mike T
fuente
Esto es brillante.
alphabetasoup
Esto no funciona para mi complicada geom: gist.github.com/ideamotor/7bd7cdee15f410ce12f3aa14ebf70177
ideamotor
funciona con ST_Snap, ala trac.osgeo.org/postgis/ticket/2192
ideamotor
2

No tengo toda la respuesta para usted, pero ST_Line_Locate_Point toma una línea y un punto como argumentos, y devuelve un número entre 0 y 1 que representa la distancia a lo largo de la línea hasta la posición más cercana al punto.

ST_Line_Substring toma una línea y dos números, cada uno entre 0 y 1, como argumentos. Los números representan posiciones en la línea como distancias fraccionarias. La función devuelve el segmento de línea que se ejecuta entre esas dos posiciones.

Al trabajar con estas dos funciones, debería poder lograr lo que quiere hacer.

Eduardo
fuente
Gracias por esto. De hecho, he resuelto este problema utilizando tu técnica y la de @rcoup. Le he dado la respuesta aceptada debido a la función que debería facilitar a los demás. Si otros quieren seguir este camino, creé una tabla temporal de las líneas que tienen puntos en ellas, con una fila para cada línea y una parada que está en ella. Agregué columnas para la salida de ST_Line_Locate_Point (line.geom, pt.geom) AS L y una función de ventana: rank () SOBRE PARTICIÓN POR line.id ORDER BY LR). Luego, IZQUIERDA EXTERIOR ÚNETE a la tabla temporal, a, para sí misma, b, donde a.id = b.id y a.LR = b.LR + 1 (continuación)
alphabetasoup
(continuación) La combinación externa permite un CASO CUANDO los campos de combinación son nulos, en cuyo caso ST_Line_Substring desde el punto hasta el final de la línea, de lo contrario ST_Line_Substring desde la referencia lineal del primer punto, hasta la referencia lineal del segundo punto (con mayor rango). Luego, se obtiene el segmento LA [inicio] con un segundo SELECT, simplemente seleccionando aquellos con un rango de 1 y calculando la ST_Line_Substring desde el ST_StartPoint de la línea hasta la referencia lineal del punto de intersección. Pop estos en la tabla, recordando mantener el line.id y voilà. Salud.
alphabetasoup
¿Puedes publicar esta respuesta como una respuesta en el código por favor? Me gustaría ver esa opción, así como soy un poco novato en SQL.
Phil Donovan
1
@PhilDonovan: hecho.
alphabetasoup
2

Me han pedido esto dos veces, perdón por el retraso. Es poco probable que esto se considere una solución concisa; Lo escribí cuando estaba un poco más abajo de la curva de aprendizaje de lo que estoy actualmente. Cualquier consejo es bienvenido, incluso los estilísticos.

--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");
alphabetasoup
fuente
0

Quiero ampliar las respuestas anteriores desde la perspectiva de un principiante. En este escenario, tiene una serie de puntos y observa cómo usarlos como una "cuchilla" para cortar una línea en segmentos. Todo este ejemplo supone que primero ajustó sus puntos a la línea y que los puntos tienen el atributo de ID único de su línea ajustada. Yo uso 'column_id "para representar la identificación única de la línea.

Primero , desea agrupar sus puntos en multipuntos cuando más de una cuchilla cae en una línea. De lo contrario, la función split_line_multipoint actúa como la función ST_Split, que no es el resultado que desea.

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;

Luego , desea dividir su red en base a estos multipuntos.

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;


Repita los pasos 1 y 2 con sus líneas que solo tienen un punto de intersección. Para hacer esto, debe actualizar el código del paso 1 a 'TENER CUENTA (*) = 1'. Cambiar el nombre de las tablas en consecuencia.


Luego , haga una tabla de líneas duplicadas y elimine las entradas con puntos en ellas.

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


Por último , une tus tres tablas 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!

Mtap1
fuente