31. Topologi

PostGIS stöder SQL/MM SQL-MM 3 Topo-Geo och Topo-Net 3 specifikationer via ett tillägg som heter postgis_topology. Du kan lära dig mer om alla funktioner och typer som tillhandahålls av detta tillägg i Manual: PostGIS Topology. Tillägget postgis_topology innehåller en annan typ av spatial kärntyp, som kallas topogeometry. Förutom den spatiala typen topogeometry hittar du funktioner för att bygga topologier och fylla i topologier.

Innan du kan börja använda topologier måste du installera tillägget postgis_topology enligt följande:

CREATE EXTENSION postgis_topology;

När du har installerat tillägget kommer du att se ett nytt schema i din databas som heter topology. Schemat topology katalogiserar alla topologier i din databas.

Schemat topology innehåller två tabeller och alla hjälpfunktioner för topologi.

  • topologi - listar alla topologier i databasen och i vilket schema de lagras

  • layer - listar alla tabellkolumner i din databas som innehåller topogeometrier

Tabellen layer är mycket lik katalogerna raster_columns, geometry_columns och geography_columns som vi lärde oss om tidigare, men specifikt för topogeometrier.

31.1. Skapa topologier

Vad exakt är en topologi och en topogeometri, och hur är de relaterade? Innan vi förklarar, låt oss börja med att skapa en topologi för att hysa våra NYC-topologiskt perfekta data med hjälp av funktionen CreateTopology och ställa in toleransen till 0,5 meter. Observera att 0,5 är i meter eftersom vårt spatiala referenssystem är State Plany NY meter.

SELECT topology.CreateTopology('nyc_topo', 26918, 0.5);

Vilket matar ut:

1

Vilket är det id som den tilldelade den nya topologin. När du har kört ovanstående kommando kommer du att se ett nytt schema i din databas som heter nyc_topo. Du kan namnge topologin vad du vill. Min konvention är att lägga till _topo i slutet för att skilja det från andra scheman jag har i min databas.

Om du utforskar tabellen topology.topology,

SELECT * FROM topology.topology;

Du kommer att få se:

id |   name    | srid  | precision | hasz
----+----------+-------+-----------+------
  1 | nyc_topo | 26918 |         0 | f
(1 row)

31.2. Lagring av topologier och topogeometrier

En topologi implementeras som ett schema i en PostgreSQL-databas. Om du utforskar schemat nyc_topo kommer du att se dessa tabeller och vyer:

  • edge - Detta är en vy som bygger på edge_data, främst för SQL/MM-överensstämmelse.

    Den har en delmängd av kolumnerna i tabellen edge_data.

  • edge_data - Innehåller alla linjestrings som utgör topologin

  • face - Innehåller en lista över alla slutna ytor som kan bildas från edge_data.

    Den innehåller inte den faktiska geometrin utan bara geometrins avgränsningsbox.

  • node - Innehåller alla start- och slutpunkter för alla kanter samt punkter som inte är anslutna till något (isolerade noder)

  • relation - detta definierar vilka element i en topologi som utgör en topogeometri.

Vad är då en topogeometri? En topogeometri är en representation av en geometri som bildas av kanter, ytor, noder och andra topogeometrier i en topologi.

Var finns en topogeometri? Den finns någon annanstans som refererar till element i en topologi via relation-tabellen. Även om vi kunde kasta topogeometrierna i vårt nyc_topo -schema, är den allmänna konventionen att definiera andra tabeller i andra scheman som har en topogeometri och också har någon annan typ av data som du kanske är intresserad av att spåra.

31.3. Varför använda topogeometrier?

Genom att använda topogeometrier håller du dina data prydliga och sammankopplade. Topogeometrier är mycket användbara för lantmäteriarbete, där du vill se till att två skiften inte överlappar varandra även om du ändrar gränserna för det ena eller att vägar förblir sammankopplade när du ändrar geometrierna som bildar dem. Geometrier lever på en egen ö, man kan duplicera dem, morfa dem. Geometrier är bekymmerslösa och bryr sig inte om andra geometrier som delar utrymme med dem. Topogeometrier följer däremot reglerna för sin topologi; de kan inte existera om det inte finns en kant, nod, yta eller annan topogeometri som definierar dem. En topogeometri hör till en och endast en topologi. En topogeometri är en relationsmodell av en geometri och när varje komponent (kanter/ytor/noder) flyttas, läggs till etc. ändras inte en topogeometris form, utan alla topogeometrier som har gemensamma komponenter.

Vi har en nyc_topo-topologi som saknar data. Låt oss fylla på den med våra NYC-data. Topologikanter, ansikten och noder kan skapas på två olika sätt.

  • Edges, Faces och Nodes kan skapas direkt med hjälp av primitiva topologifunktioner.

  • Edges, Faces och Nodes kan bildas genom att skapa topogeometrier. När en topogeometri skapas från en geometri och det saknas kanter, ytor eller noder som matchar dess koordinater, skapas nya kanter, ytor och noder som en del av processen.

31.4. Definiera kolumner för topogeometri och skapa topogeometrier

Det vanligaste sättet att fylla i topologier är att skapa topogeometrier. Vi börjar med att skapa en tabell som innehåller stadsdelar och lägger sedan till en topogeometrikolumn med hjälp av funktionen AddTopoGeometryColumn.

CREATE TABLE nyc_neighborhoods_t(boroname varchar(43), name varchar(67),
  CONSTRAINT pk_nyc_neighborhoods_t PRIMARY KEY(boroname,name) );
SELECT topology.AddTopoGeometryColumn('nyc_topo', 'public', 'nyc_neighborhoods_t',
  'topo', 'POLYGON') As  layer_id;

Utdata av ovanstående är:

layer_id
--------
1

Nu är vi redo att fylla i vår tabell. Det är bäst att se till att geometrierna är giltiga innan du lägger till dem, annars får du felmeddelanden som SQLMM geometry is not simple.

Så låt oss börja med att lägga till giltiga sådana. Det 1 som används här hänvisar till det layer_id som genererades från den tidigare frågan. Om du inte känner till lager-ID:t kan du leta upp det med hjälp av funktionen FindLayer som vi använder i senare exempel.

I de här exemplen använder du funktionen toTopoGeom för att konvertera en geometri till dess topogeometriska motsvarighet. toTopoGeom-funktionen sköter en hel del bokföring åt dig.

Funktionen toTopoGeom inspekterar den geometri som skickas in och injicerar noder, kanter och ytor efter behov i din topologi för att bilda geometrins form. Den lägger sedan till relationer i tabellen relation som definierar hur den nya topogeometrin är relaterad till dessa nya och befintliga topologielement. I vissa fall kan det finnas delar av geometrin eller så måste befintliga delar delas upp för att bilda den nya geometrin.

INSERT INTO nyc_neighborhoods_t(boroname,name, topo)
SELECT boroname, name,  topology.toTopoGeom(geom, 'nyc_topo', 1)
  FROM nyc_neighborhoods
  WHERE ST_ISvalid(geom);

Ovanstående steg bör ta 3-4 sekunder. Låt oss nu lägga till de ogiltiga:

INSERT INTO nyc_neighborhoods_t(boroname,name, topo)
SELECT boroname, name,  topology.toTopoGeom(
  ST_UnaryUnion(
    ST_CollectionExtract(
      ST_MakeValid(geom), 3)
      ), 'nyc_topo', 1)
  FROM nyc_neighborhoods
  WHERE NOT ST_ISvalid(geom);

Ovanstående bör ta cirka 300-400 ms.

Nu har vi data i vår topologi. En snabb kontroll visar att nyc_topo.edge, nyc_topo.node och nyc_topo.face har data:

SELECT 'edge' AS name, count(*)
  FROM nyc_topo.edge
UNION ALL
SELECT 'node' AS name, count(*)
  FROM nyc_topo.node
UNION ALL
SELECT 'face' AS name, count(*)
  FROM nyc_topo.face;

utmatning:

name | count
------+-------
edge |   580
node |   396
face |   218
(3 rows)

Now we can express declaratively that boros are formed from a collection of neighborhoods by defining a column called topo in nyc_boros_t table that is of type POLYGON and is a collection of other topogeometries from nyc_neighborhoods_t.topo column.

CREATE TABLE nyc_boros_t(boroname varchar(43),
  CONSTRAINT pk_nyc_boros_t PRIMARY KEY(boroname) );
SELECT topology.AddTopoGeometryColumn('nyc_topo', 'public', 'nyc_boros_t',
  'topo', 'POLYGON',
    (topology.FindLayer('public', 'nyc_neighborhoods_t', 'topo')).layer_id
        ) AS  layer_id;

Vilket matar ut:

För att fylla i denna nya tabell använder vi funktionen CreateTopoGeom. I stället för att börja med geometrier för att bilda en ny topogeometri, börjar CreateTopoGeom med befintliga topologielement som kan vara primitiver eller andra topogeometrier för att definiera en ny topogeometri.

INSERT INTO nyc_boros_t(boroname, topo)
SELECT n.boroname,
  topology.CreateTopoGeom('nyc_topo',
  3,  (topology.FindLayer('public', 'nyc_boros_t', 'topo')).layer_id ,
    topology.TopoElementArray_Agg( ARRAY[ (n.topo).id, (n.topo).layer_id ]::topoelement ) )
  FROM nyc_neighborhoods_t AS n
GROUP BY n.boroname;

Vilket kommer att infoga 5 poster som motsvarar stadsdelarna i New York.

Observera

Om du använder PostGIS 3.4 eller senare kan du använda det nya kastet för att kasta en topogeometri till ett topoelement och ersätta topology.TopoElementArray_Agg( ARRAY[ (n.topo).id, (n.topo).layer_id ]::topoelement ) ) i exemplet ovan med det kortare topology.TopoElementArray_Agg( n.topo::topoelement )

Om du vill visa dessa i pgAdmin kan du omvandla topogeometrin till en geometri på följande sätt:

SELECT boroname, topo::geometry AS geom
 FROM nyc_boros_t;

Utdata kommer att se ut som nedan:

_images/boros_topogeom.png

Om du tänker att det är en enda röra, så är det en enda röra. Detta är vad som händer efter många cykler av förenkling och andra geometriska processer där varje geometri behandlas som en separat enhet. Du får luckor, du får dinglande öar och stadsdelar som inkräktar på varandras territorium.

Som tur är kan vi använda topologi för att städa upp i den här röran och hjälpa oss att behålla bra, rena och sammanhängande data.

Låt oss ta på oss lantmäterihatten och ställa frågan: om vi delar in våra tomter i distrikt (boros eller stadsdelar) så att varje distrikt kan gränsa till andra distrikt men inte ska ha något gemensamt område, är det då rimligt att distrikten har gemensamma områden? Nej, det är inte vettigt. Och här är vi med våra data som pekar på att vissa områden tillhör mer än ett grannskap eller mer än en stadsdel.

Låt oss först titta på Boros och leta efter stadsdelar som delar element gemensamt:

SELECT te, array_agg(DISTINCT b.boroname)
 FROM nyc_boros_t AS b, topology.GetTopoGeomelements(topo) AS te
 GROUP BY te
 HAVING count(DISTINCT b.boroname) > 1;

Utdata är:

  te    |     array_agg
--------+-------------------
{44,3}  | {Brooklyn,Queens}
{51,3}  | {Brooklyn,Queens}
{76,3}  | {Brooklyn,Queens}
{114,3} | {Brooklyn,Queens}
{117,3} | {Brooklyn,Queens}
(5 rows)

Which tells us that Queens and Brooklyn are in the middle of border wars. In this query we use the GetTopoGeomElements function to declaratively inspect what components are shared across boroughs.

What is returned are a set of topolements. A topoelement is represented as an array of 2 integers with the first number being the id of the element, and the second, being the layer (or primitive type) of the element. PostGIS GetTopoElements returns the primitives of a topogeometry with types number 1-3 corresponding to (1: nodes, 2: edges, and 3: faces). All the topoelements for neighborhoods and boroughs are type 3, which corresponds to a topological face. We can use the ST_GetFaceGeometry to get a visual representation of these shared faces as follows:

SELECT te, t.geom, ST_Area(t.geom) AS area, array_agg(DISTINCT d.boroname) AS shared_boros
FROM nyc_boros_t AS d, topology.GetTopoGeomelements(topo) AS te
  , topology.ST_GetFaceGeometry('nyc_topo',te[1]) AS t(geom)
GROUP BY te, t.geom
HAVING count(DISTINCT d.boroname) > 1
ORDER BY area;

Resultatet blir 5 rader som motsvarar gränstvister mellan Queens och Brooklyn.

Om vi tittar på våra grannskap ser vi en liknande historia men med 44 gränstvister:

SELECT te, t.geom, ST_Area(t.geom) AS area, array_agg(DISTINCT d.name) AS shared_d
FROM nyc_neighborhoods_t AS d, topology.GetTopoGeomelements(d.topo) AS te
  , topology.ST_GetFaceGeometry('nyc_topo',te[1]) AS t(geom)
GROUP BY te, t.geom
HAVING count(DISTINCT d.name) > 1
ORDER BY area;

Eftersom stadsdelar är en sammanslagning av grannskap kan vi lösa stadsdelsfrågan genom att lösa grannskapets gränstvister.

Det finns ett antal sätt att åtgärda detta. Vi skulle kunna gå ut och fråga folk i vilket grannskap de tror att de bor. Alternativt kan vi bara tilldela landområden till grannskapet med minst yta eller till den högsta budgivaren.

Att ta bort element från topogeometrier hanteras med hjälp av funktionen TopoGeom_remElement. Så låt oss sätta igång och ta bort duplicerade element från stadsdelar med störst yta enligt följande:

WITH to_remove AS (SELECT te, MAX( ST_Area(d.topo::geometry) ) AS max_area, array_agg(DISTINCT d.name) AS shared_d
  FROM nyc_neighborhoods_t AS d, topology.GetTopoGeomelements(d.topo) AS te
    , topology.ST_GetFaceGeometry('nyc_topo',te[1]) AS t(geom)
  GROUP BY te
  HAVING count(DISTINCT d.name) > 1)
  UPDATE nyc_neighborhoods_t AS d SET topo = TopoGeom_remElement(topo, te)
  FROM to_remove
  WHERE d.name = ANY(to_remove.shared_d)
    AND ST_Area(d.topo::geometry) = to_remove.max_area;

Resultatet av ovanstående är att 29 stadsdelar uppdaterades. Om du kör om gränstvistfrågorna för stadsdelar och distrikt kommer du att upptäcka att du inte har några fler gränstvister.

We do still have gaps of empty space between neighborhoods caused by intensive simplification. Such issues can be fixed by directly editing the topology using the Topology Editor family of functions and/or filling in the holes and assigning those to neighborhoods.