Teilen Sie Linien basierend auf Punkten in nicht überlappende Teilmengen auf


10

Bei einer Tabelle mit Liniengeometrie und einem oder mehreren Punkten, die in einer separaten Tabelle an dieser Linie ausgerichtet sind, möchte ich jede Linie mit einem oder mehreren Schnittpunkten an jeder der Stellen teilen, an denen die Linie einen Punkt schneidet.

Zum Beispiel gibt es eine Linie L mit drei Schnittpunkten A, B und C in der Reihenfolge entlang der Liniengeometrie. Ich möchte L als vier verschiedene Geometrien zurückgeben: vom Anfang von L nach A, von A nach B entlang L, von B nach C entlang L und von C bis zum Ende von L.

In der Vergangenheit habe ich diese Aufgabe formschön verwendet, was ein lineares Referenzierungsproblem darstellt ( http://sgillies.net/blog/1040/shapely-recipes/ ). Dies wäre jedoch in diesem Fall mit vielen Millionen Linien und Punkten nicht praktikabel. Stattdessen suche ich nach einer Lösung mit PostgreSQL / PostGIS.

Beachten Sie, dass Punkte auf einer Linie liegen müssen. Ferner kann sich ein Punkt gültig am Anfang oder Ende einer Linie befinden. In diesem Fall muss die Linie nicht geteilt werden (es sei denn, es gibt andere Punkte, die nicht mit den Start- oder Endpunkten derselben Linie übereinstimmen). Die Teilmengenlinien müssen ihre Richtung und ihre Attribute beibehalten, aber die Attribute der Punktmerkmale spielen keine Rolle.

Antworten:


7

Die ST_Split PostGIS-Funktion ist wahrscheinlich genau das, was Sie wollen.

PostGIS 2.2+ unterstützt jetzt Multi * -Geometrien in ST_Split.

Lesen Sie für ältere Versionen von PostGIS weiter:


Um eine einzelne Linie zu erhalten, die durch mehrere Punkte geteilt wird, können Sie so etwas wie diese Mehrpunkt-Wrapper- Funktion plpgsql verwenden. Ich habe es nur auf den Fall "geteilte (Mehrfach-) Linien mit (Mehrfach-) Punkten" unten vereinfacht:

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;

Verwenden Sie dann ST_Collect , um eine Mehrpunktgeometrie zum Schneiden zu erstellen, und erstellen Sie sie entweder manuell aus den Eingaben:

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

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

Oder aggregieren Sie es aus einer Unterabfrage:

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

Ich habe zunächst ST_Split ausprobiert und war überrascht, als ich feststellte, dass es keine Mehrpunktgeometrie akzeptiert. Ihre Funktion scheint diese Lücke zu füllen, gibt jedoch für den Beispiel-Mehrpunktfall leider NULL zurück. (Es funktioniert gut für den (Einzel-) Punkt.) Ich habe jedoch IF Blade_geometry_type NOT ILIKE '% LINESTRING' THEN in IF Blade_geometry_type ILIKE '% LINESTRING' THEN in Ihrer Funktion geändert und das erwartete und korrekte Ergebnis 'GEOMETRYCOLLECTION' erhalten. Ich bin jedoch noch ziemlich neu bei PostGIS. Ist diese Änderung also sinnvoll?
Alphabetasoup

Entschuldigung, hätte sein sollen IF geom_geometry_type NOT ILIKE '%LINESTRING' THEN- ich habe es bearbeitet.
Gruppe

1
Ah ich sehe. Danke, das ist eine großartige Lösung. Sie sollten dies als Beitrag zu ST_Split vorschlagen, damit es Multiline und Multipoint verarbeiten kann, sofern dies nicht bereits in der PostGIS-Pipeline enthalten ist.
Alphabetasoup

3
ST_Splitunterstützt Multi * Blades in postgis 2.2und über postgis.net/docs/ST_Split.html
Raphael

3

Upgrade auf PostGIS 2.2 , wo ST_Split erweitert wurde, um die Aufteilung durch eine mehrzeilige, mehrpunktige oder ( mehr- ) Polygongrenze zu unterstützen.

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

Das ist brilliant.
Alphabetasoup

Dies funktioniert nicht für mein kompliziertes Geom: gist.github.com/ideamotor/7bd7cdee15f410ce12f3aa14ebf70177
ideamotor

es funktioniert mit ST_Snap, ala trac.osgeo.org/postgis/ticket/2192
ideamotor

2

Ich habe nicht die ganze Antwort für Sie, aber ST_Line_Locate_Point verwendet eine Linie und einen Punkt als Argumente und gibt eine Zahl zwischen 0 und 1 zurück, die den Abstand entlang der Linie zu der Position darstellt, die dem Punkt am nächsten liegt.

ST_Line_Substring verwendet eine Zeile und zwei Zahlen zwischen 0 und 1 als Argumente. Die Zahlen repräsentieren Positionen auf der Linie als Bruchabstände. Die Funktion gibt das Liniensegment zurück, das zwischen diesen beiden Positionen verläuft.

Wenn Sie mit diesen beiden Funktionen arbeiten, sollten Sie in der Lage sein, das zu erreichen, was Sie tun möchten.


Danke dafür. Ich habe dieses Problem tatsächlich mit Ihrer Technik sowie der von @rcoup gelöst. Ich habe ihm die akzeptierte Antwort gegeben, aufgrund der Funktion, die es anderen leicht machen sollte. Wenn andere diesen Pfad beschreiten möchten, habe ich eine temporäre Tabelle der Linien erstellt, auf denen Punkte vermerkt sind, mit einer Zeile für jede Linie und einem Stopp darauf. Ich habe Spalten für die Ausgabe von ST_Line_Locate_Point (line.geom, pt.geom) AS L und eine Fensterfunktion hinzugefügt: rank () OVER PARTITION BY line.id ORDER BY LR). Dann LEFT OUTER JOIN die temporäre Tabelle, a, für sich, b, wobei a.id = b.id und a.LR = b.LR + 1 (Fortsetzung)
alphabetasoup

(Fortsetzung) Die äußere Verknüpfung ermöglicht einen FALL, WENN die Verknüpfungsfelder null sind. In diesem Fall ist ST_Line_Substring vom Punkt bis zum Ende der Linie, andernfalls ST_Line_Substring von der linearen Referenz des ersten Punkts bis zur linearen Referenz des zweiten Punkts (mit höherem Rang). Das Abrufen des LA-Segments [start] erfolgt dann mit einem zweiten SELECT, wobei nur diejenigen mit einem Rang von 1 ausgewählt und der ST_Line_Substring vom ST_StartPoint der Linie bis zur linearen Referenz des Schnittpunkts berechnet werden. Stellen Sie diese in die Tabelle und denken Sie daran, die line.id und voilà beizubehalten. Prost.
Alphabetasoup

Können Sie diese Antwort bitte als Antwort im Code posten? Ich würde mir diese Option gerne ansehen und bin ein bisschen ein Neuling in SQL.
Phil Donovan

1
@PhilDonovan: fertig.
Alphabetasoup

2

Ich wurde jetzt zweimal danach gefragt, also entschuldige die Verzögerung. Es ist unwahrscheinlich, dass dies als knappe Lösung angesehen wird. Ich habe es geschrieben, als ich etwas weiter unten in der Lernkurve war als ich es derzeit bin. Alle Tipps sind willkommen, auch stilistische.

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

Ich möchte die obigen Antworten aus der Perspektive eines Anfängers erweitern. In diesem Szenario haben Sie eine Reihe von Punkten und beobachten, wie Sie diese als "Klinge" verwenden, um Linien in Segmente zu schneiden. In diesem ganzen Beispiel wird davon ausgegangen, dass Sie Ihre Punkte zuerst an der Linie gefangen haben und dass die Punkte das eindeutige ID-Attribut von ihrer Linie haben. Ich benutze 'column_id ", um die eindeutige ID der Zeile darzustellen.

Zunächst möchten Sie Ihre Punkte in mehrere Punkte gruppieren, wenn mehr als eine Klinge auf eine Linie fällt. Andernfalls verhält sich die Funktion split_line_multipoint wie die Funktion ST_Split, was nicht das gewünschte Ergebnis ist.

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;

Anschließend möchten Sie Ihr Netzwerk basierend auf diesen Multipunkten aufteilen.

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;


Wiederholen Sie die Schritte 1 und 2 mit Ihren Linien, die nur einen Schnittpunkt haben. Dazu sollten Sie den Code von Schritt 1 auf 'HAVING COUNT (*) = 1' aktualisieren. Benennen Sie die Tabellen entsprechend um.


Erstellen Sie als Nächstes eine doppelte Linientabelle und löschen Sie die Einträge mit Punkten darauf.

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


Zuletzt verbinden Sie Ihre drei Tabellen mit 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!

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.