31. الطوبولوجيا

تدعم PostGIS مواصفات SQL/MM SQL-MM 3 Topo-Geo و Topo-Net 3 عبر امتداد يُسمى postgis_topology. يمكنك التعرف على جميع الدوال والأنواع التي يوفرها هذا الامتداد في الدليل: PostGIS Topology. يتضمن الامتداد postgis_topology نوعًا مكانيًا أساسيًا آخر يُسمى topogeometry. وإضافة إلى النوع المكاني topogeometry، ستجد دوالًا لبناء الطوبولوجيات وتعبئتها بالبيانات.

قبل أن تبدأ باستخدام الطوبولوجيات، يجب أن تثبّت الامتداد postgis_topology كما يلي:

CREATE EXTENSION postgis_topology;

بعد تثبيت الامتداد، سترى مخططًا جديدًا في قاعدة بياناتك يُسمى topology. يفهرس المخطط topology جميع الطوبولوجيات في قاعدة بياناتك.

يحتوي المخطط topology على جدولين وجميع الدوال المساعدة الخاصة بالطوبولوجيا.

  • topology - يسرد جميع الطوبولوجيات في قاعدة بياناتك، والمخطط الذي تُخزَّن فيه كل منها

  • layer - يسرد جميع أعمدة الجداول في قاعدة بياناتك التي تحتوي على topogeometries

الجدول layer مشابه جدًا للفهارس raster_columns و geometry_columns و geography_columns التي تعرّفنا عليها سابقًا، لكنه مخصص لـ topogeometries.

31.1. إنشاء الطوبولوجيات

ما الطوبولوجيا وما الـ topogeometry بالضبط، وما العلاقة بينهما؟ قبل أن نشرح، لنبدأ بإنشاء طوبولوجيا لاحتواء بيانات مدينة نيويورك المثالية طوبولوجيًا باستخدام الدالة CreateTopology، ونضبط التفاوت المسموح (tolerance) على 0.5 متر. لاحظ أن القيمة 0.5 بالمتر لأن نظامنا المرجعي المكاني هو State Plane NY بوحدة المتر.

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

والذي يُخرج:

1

وهو المعرّف الذي أُسند إلى الطوبولوجيا الجديدة. بمجرد تشغيل الأمر أعلاه، سترى مخططًا جديدًا في قاعدة بياناتك يُسمى nyc_topo. يمكنك تسمية الطوبولوجيا بأي اسم تريده. وعرفي الخاص هو إضافة _topo في النهاية لتمييزها عن المخططات الأخرى في قاعدة بياناتي.

إذا استكشفت الجدول topology.topology،

SELECT * FROM topology.topology;

فسترى:

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

31.2. تخزين الطوبولوجيات و topogeometries

تُنفَّذ الطوبولوجيا على هيئة مخطط في قاعدة بيانات PostgreSQL. وإذا استكشفت المخطط nyc_topo فسترى هذه الجداول والعروض (views):

  • edge - وهو عرض (view) مبني على edge_data، وذلك أساسًا للتوافق مع SQL/MM.

    يحتوي على مجموعة جزئية من أعمدة الجدول edge_data.

  • edge_data - يحتوي على جميع الخطوط (linestrings) التي تتكون منها الطوبولوجيا

  • face - يحتوي على قائمة بجميع الأسطح المغلقة التي يمكن تكوينها من edge_data.

    لا يحتوي على الشكل الهندسي الفعلي، بل على المستطيل المحيط بالشكل الهندسي فقط.

  • node - يحتوي على جميع نقاط البداية والنهاية لجميع الحواف، إضافة إلى النقاط غير المتصلة بأي شيء (العقد المعزولة)

  • relation - يحدد عناصر الطوبولوجيا التي يتكون منها كل topogeometry.

إذن ما الـ topogeometry؟ الـ topogeometry تمثيل لشكل هندسي مكوَّن من الحواف والأوجه والعقد و topogeometries أخرى في طوبولوجيا ما.

أين يقع الـ topogeometry؟ يقع في مكان آخر يشير إلى عناصر الطوبولوجيا عبر الجدول relation. ورغم أنه يمكننا وضع الـ topogeometries في المخطط nyc_topo، فإن العُرف السائد هو تعريف جداول أخرى في مخططات أخرى تحتوي على topogeometry، وتحتوي أيضًا على أي نوع آخر من البيانات قد ترغب في تتبّعه.

31.3. لماذا نستخدم topogeometries؟

يحافظ استخدام topogeometries على بياناتك مرتبة ومتصلة. وهي مفيدة جدًا في أعمال المساحة العقارية (Cadastral)، حيث تريد التأكد من أن قطعتي أرض لا تتداخلان حتى لو غيّرت حدود إحداهما، أو تريد التأكد من بقاء الطرق متصلة عند تغيير الأشكال الهندسية المكوِّنة لها. فالأشكال الهندسية تعيش في جزيرة خاصة بها؛ يمكنك نسخها وتحويرها. وهي غير مبالية، لا تكترث بالأشكال الهندسية الأخرى التي تشاركها الحيّز. أما الـ topogeometries فعلى النقيض تتبع قواعد طوبولوجيتها؛ فلا يمكن أن توجد ما لم تكن هناك حافة أو عقدة أو وجه أو topogeometry آخر يعرّفها. وينتمي كل topogeometry إلى طوبولوجيا واحدة فقط لا غير. والـ topogeometry نموذج علائقي للشكل الهندسي، ولذلك عندما تُنقل مكوناته (الحواف/الأوجه/العقد) أو تُضاف وما إلى ذلك، فإنها لا تغيّر شكل topogeometry واحد فقط، بل جميع الـ topogeometries التي تشترك في مكونات.

لدينا طوبولوجيا nyc_topo خالية من أي بيانات. لنملأها ببيانات مدينة نيويورك. يمكن إنشاء حواف الطوبولوجيا وأوجهها وعقدها بطريقتين رئيسيتين.

  • يمكن إنشاء الحواف والأوجه والعقد مباشرة باستخدام دوال العناصر الأولية (primitives) للطوبولوجيا.

  • يمكن تكوين الحواف والأوجه والعقد بإنشاء topogeometries. فعند إنشاء topogeometry من شكل هندسي مع غياب حواف أو أوجه أو عقد تطابق إحداثياته، تُنشأ حواف وأوجه وعقد جديدة كجزء من العملية.

31.4. تعريف أعمدة topogeometry وإنشاء topogeometries

الطريقة الأكثر شيوعًا لتعبئة الطوبولوجيات هي إنشاء topogeometries. لنبدأ بإنشاء جدول لاحتواء الأحياء، ثم نضيف عمود topogeometry باستخدام الدالة 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;

مخرجات ما سبق هي:

layer_id
--------
1

نحن الآن جاهزون لتعبئة جدولنا. من الأفضل التأكد من صلاحية أشكالك الهندسية قبل إضافتها، وإلا فستحصل على أخطاء مثل SQLMM geometry is not simple.

لنبدأ إذن بإضافة الأشكال الصالحة. يشير الرقم 1 المستخدم هنا إلى layer_id الذي وُلِّد من الاستعلام السابق. وإذا لم تكن تعرف معرّف الطبقة، فيمكنك البحث عنه باستخدام الدالة FindLayer التي سنستخدمها في أمثلة لاحقة.

في هذه الأمثلة ستستخدم الدالة toTopoGeom لتحويل شكل هندسي إلى ما يكافئه من topogeometry. وتتولى الدالة toTopoGeom عنك الكثير من أعمال المتابعة والتنظيم.

تفحص الدالة toTopoGeom الشكل الهندسي المُمرَّر إليها وتُدخل العقد والحواف والأوجه حسب الحاجة في طوبولوجيتك لتكوين شكل الشكل الهندسي. ثم تضيف علاقات إلى الجدول relation تحدد كيفية ارتباط هذا الـ topogeometry الجديد بعناصر الطوبولوجيا الجديدة والموجودة. وفي بعض الحالات قد تكون أجزاء من الشكل الهندسي موجودة مسبقًا، أو قد يلزم تقسيم أجزاء موجودة لتكوين الشكل الهندسي الجديد.

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

يُفترض أن تستغرق الخطوة أعلاه 3-4 ثوانٍ. لنضف الآن الأشكال غير الصالحة:

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

يُفترض أن يستغرق ما سبق نحو 300-400ms.

أصبحت لدينا الآن بيانات في طوبولوجيتنا. وسيُظهر فحص سريع أن nyc_topo.edge و nyc_topo.node و nyc_topo.face تحتوي على بيانات:

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;

المخرجات:

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

يمكننا الآن أن نعبّر بشكل تصريحي عن أن المقاطعات تتكون من مجموعة من الأحياء، وذلك بتعريف عمود يُسمى topo في الجدول nyc_boros_t من النوع POLYGON، يكون مجموعة من topogeometries أخرى من العمود nyc_neighborhoods_t.topo.

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;

والذي يُخرج:

لتعبئة هذا الجدول الجديد، سنستخدم الدالة CreateTopoGeom. فبدلًا من البدء بأشكال هندسية لتكوين topogeometry جديد، تبدأ CreateTopoGeom بعناصر طوبولوجيا موجودة، قد تكون عناصر أولية أو topogeometries أخرى، لتعريف topogeometry جديد.

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;

والذي سيُدرج 5 سجلات تقابل مقاطعات نيويورك.

ملاحظة

إذا كنت تستخدم PostGIS 3.4 أو أحدث، فيمكنك استخدام التحويل الجديد لتحويل topogeometry إلى topoelement، واستبدال topology.TopoElementArray_Agg( ARRAY[ (n.topo).id, (n.topo).layer_id ]::topoelement ) ) في المثال أعلاه بالصيغة الأقصر topology.TopoElementArray_Agg( n.topo::topoelement )

لعرضها في pgAdmin، يمكنك تحويل الـ topogeometry إلى شكل هندسي كما يلي:

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

ستبدو المخرجات كما يلي:

_images/boros_topogeom.png

إن كنت تفكر في أنها فوضى عارمة، فنعم، إنها فوضى عارمة. هذا ما يحدث بعد دورات عديدة من التبسيط وعمليات الأشكال الهندسية الأخرى التي يُعامَل فيها كل شكل هندسي كوحدة منفصلة. فتظهر فجوات، وجزر متدلية، وأحياء تتعدى على أراضي بعضها.

لحسن الحظ يمكننا استخدام الطوبولوجيا لتنظيف هذه الفوضى ولمساعدتنا في الحفاظ على بيانات جيدة ونظيفة ومتصلة.

لنعتمر قبعة المسّاح ونطرح السؤال: إذا كنا نقسّم قطع أراضينا إلى مناطق (مقاطعات أو أحياء) بحيث يمكن لكل منطقة أن تتاخم مناطق أخرى لكن دون أن تشترك معها في أي مساحة، فهل من المنطقي أن تكون للمناطق مساحات مشتركة؟ لا، ليس منطقيًا. ومع ذلك ها هي بياناتنا تشير إلى أن بعض المساحات تنتمي إلى أكثر من حي أو أكثر من مقاطعة.

لننظر أولًا إلى المقاطعات ونبحث عن الأحياء التي تشترك في عناصر:

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;

المخرجات هي:

  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)

وهذا يخبرنا بأن Queens و Brooklyn في خضم حروب حدودية. نستخدم في هذا الاستعلام الدالة GetTopoGeomElements لفحص المكونات المشتركة بين المقاطعات بشكل تصريحي.

ما يُعاد هو مجموعة من topoelements. يُمثَّل الـ topoelement كمصفوفة من عددين صحيحين، الأول هو معرّف العنصر، والثاني هو الطبقة (أو نوع العنصر الأولي) للعنصر. تعيد الدالة GetTopoElements في PostGIS العناصر الأولية لـ topogeometry بأنواع مرقّمة من 1 إلى 3 تقابل (1: العقد، 2: الحواف، 3: الأوجه). وجميع الـ topoelements للأحياء والمقاطعات من النوع 3، الذي يقابل وجهًا طوبولوجيًا. ويمكننا استخدام ST_GetFaceGeometry للحصول على تمثيل مرئي لهذه الأوجه المشتركة كما يلي:

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;

ستكون النتيجة 5 صفوف تقابل النزاعات الحدودية بين Queens و Brooklyn.

إذا نظرنا إلى أحيائنا فسنرى قصة مشابهة لكن مع 44 نزاعًا حدوديًا:

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;

بما أن المقاطعات تجميع للأحياء، يمكننا إصلاح مشكلة المقاطعات بإصلاح النزاعات الحدودية بين الأحياء.

هناك عدة طرق يمكننا بها إصلاح ذلك. يمكننا أن نخرج في مسح ميداني نسأل الناس عن الحي الذي يظنون أنهم يقفون فيه. أو بدلًا من ذلك يمكننا ببساطة أن نُسند الشرائح الضيقة من الأرض إلى الحي الأصغر مساحة أو إلى صاحب أعلى عرض.

تتم إزالة العناصر من الأشكال الهندسية الطوبولوجية (Topogeometries) باستخدام الدالة TopoGeom_remElement. فلنبدأ إذن بإزالة العناصر المكررة من الأحياء ذات المساحة الأكبر كما يلي:

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;

نتيجة ما سبق هي تحديث 29 حيًّا. وإذا أعدت تشغيل استعلامات النزاعات الحدودية للأحياء والأقضية (boroughs)، فستجد أنه لم تعد هناك أي نزاعات حدودية.

لا تزال لدينا فجوات من المساحات الفارغة بين الأحياء ناتجة عن التبسيط المكثف. ويمكن إصلاح مثل هذه المشكلات بتحرير الطوبولوجيا مباشرة باستخدام عائلة دوال محرر الطوبولوجيا و/أو بملء الثقوب وإسنادها إلى الأحياء.