18. الإحداثيات الجغرافية (Geography)

من الشائع جدًا أن تكون لدينا بيانات إحداثياتها "جغرافية" أو "خطوط عرض/خطوط طول".

على عكس الإحداثيات في Mercator أو UTM أو Stateplane، فإن الإحداثيات الجغرافية ليست إحداثيات ديكارتية. فالإحداثيات الجغرافية لا تمثّل مسافة خطية من نقطة أصل كما تُرسم على مستوٍ، بل تصف هذه الإحداثيات الكروية إحداثيات زاوية على الكرة الأرضية. وفي الإحداثيات الكروية تُحدَّد النقطة بزاوية الدوران من خط زوال مرجعي (خط الطول)، والزاوية من خط الاستواء (خط العرض).

_images/cartesian_spherical.jpg

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

على سبيل المثال، إليك إحداثيات لوس أنجلوس وباريس.

  • لوس أنجلوس: POINT(-118.4079 33.9434)

  • باريس: POINT(2.3490 48.8533)

يحسب ما يلي المسافة بين لوس أنجلوس وباريس باستخدام الدالة الديكارتية القياسية في PostGIS ST_Distance(geometry, geometry). لاحظ أن SRID ذا القيمة 4326 يعلن نظامًا مرجعيًا مكانيًا جغرافيًا.

SELECT ST_Distance(
  'SRID=4326;POINT(-118.4079 33.9434)'::geometry, -- Los Angeles (LAX)
  'SRID=4326;POINT(2.5559 49.0083)'::geometry     -- Paris (CDG)
  );
121.898285970107

آها! 122! ولكن ماذا يعني ذلك؟

وحدات النظام المرجعي المكاني 4326 هي الدرجات. إذن إجابتنا هي 122 درجة. ولكن (مرة أخرى) ماذا يعني ذلك؟

على سطح الكرة، يتفاوت حجم «الدرجة المربعة» الواحدة تفاوتًا كبيرًا، إذ يصغر كلما ابتعدت عن خط الاستواء. تخيّل خطوط الطول (الخطوط الرأسية) على الكرة الأرضية وهي تقترب من بعضها كلما اتجهت نحو القطبين. لذلك فإن مسافة 122 درجة لا تعني شيئًا، إنها رقم لا معنى له.

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

يوفّر PostGIS هذه الوظيفة من خلال النوع geography.

ملاحظة

تتبع قواعد البيانات المكانية المختلفة أساليب مختلفة في «التعامل مع الإحداثيات الجغرافية»

  • يحاول Oracle إخفاء الفروق بإجراء الحسابات الجغرافية بشكل شفاف عندما يكون SRID جغرافيًا.

  • يستخدم SQL Server نوعين مكانيين: "STGeometry" للبيانات الديكارتية و"STGeography" للإحداثيات الجغرافية.

  • Informix Spatial امتداد ديكارتي خالص لـ Informix، بينما Informix Geodetic امتداد جغرافي خالص.

  • على غرار SQL Server، يستخدم PostGIS نوعين: "geometry" و"geography".

باستخدام النوع geography بدلًا من geometry، لنحاول مجددًا قياس المسافة بين لوس أنجلوس وباريس.

SELECT ST_Distance(
  'SRID=4326;POINT(-118.4079 33.9434)'::geography, -- Los Angeles (LAX)
  'SRID=4326;POINT(2.5559 49.0083)'::geography     -- Paris (CDG)
  );
9124665.27317673

رقم كبير! جميع القيم المُعادة من حسابات geography تكون بالأمتار، لذا فإجابتنا هي 9125km.

كانت الإصدارات الأقدم من PostGIS تدعم حسابات أساسية جدًا على الكرة باستخدام الدالة ST_Distance_Spheroid(point, point, measurement). لكن ST_Distance_Spheroid محدودة إلى حد كبير؛ فهي تعمل على النقاط فقط ولا توفّر أي دعم للفهرسة عبر القطبين أو خط التاريخ الدولي.

تتضح الحاجة إلى دعم الأشكال الهندسية غير النقطية عند طرح سؤال مثل: «إلى أي مدى ستقترب رحلة جوية من لوس أنجلوس إلى باريس من آيسلندا؟»

_images/lax_cdg.jpg

إن التعامل مع الإحداثيات الجغرافية على مستوٍ ديكارتي (الخط البنفسجي) يعطي إجابة خاطئة جدًا بالفعل! أما استخدام مسارات الدوائر العظمى (الخطوط الحمراء) فيعطي الإجابة الصحيحة. إذا حوّلنا رحلتنا LAX-CDG إلى خط وحسبنا المسافة إلى نقطة في آيسلندا باستخدام geography فسنحصل على الإجابة الصحيحة (تذكّر) بالأمتار.

SELECT ST_Distance(
  ST_GeographyFromText('LINESTRING(-118.4079 33.9434, 2.5559 49.0083)'), -- LAX-CDG
  ST_GeographyFromText('POINT(-22.6056 63.9850)')                        -- Iceland (KEF)
);
502454.906643729

إذن فإن أقرب اقتراب من آيسلندا (مقيسًا من مطارها الدولي) على مسار LAX-CDG هو مسافة صغيرة نسبيًا تبلغ 502km.

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

_images/lax_nrt.png
SELECT ST_Distance(
  ST_GeometryFromText('Point(-118.4079 33.9434)'),  -- LAX
  ST_GeometryFromText('Point(139.733 35.567)'))     -- NRT (Tokyo/Narita)
    AS geometry_distance,
ST_Distance(
  ST_GeographyFromText('Point(-118.4079 33.9434)'), -- LAX
  ST_GeographyFromText('Point(139.733 35.567)'))    -- NRT (Tokyo/Narita)
    AS geography_distance;
 geometry_distance | geography_distance
-------------------+--------------------
  258.146005837336 |   8833954.76996256

18.1. استخدام Geography

لتحميل بيانات geometry في جدول geography، يجب أولًا إسقاط الشكل الهندسي إلى EPSG:4326 (خط الطول/خط العرض)، ثم تحويله إلى geography. تحوّل الدالة ST_Transform(geometry,srid) الإحداثيات إلى إحداثيات جغرافية، بينما تقوم الدالة Geography(geometry) أو اللاحقة ::geography بـ«تحويل النوع» (cast) إلى geography.

CREATE TABLE nyc_subway_stations_geog AS
SELECT
  ST_Transform(geom,4326)::geography AS geog,
  name,
  routes
FROM nyc_subway_stations;

إنشاء فهرس مكاني على جدول geography مطابق تمامًا لإنشائه على جدول geometry:

CREATE INDEX nyc_subway_stations_geog_gix
ON nyc_subway_stations_geog USING GIST (geog);

يكمن الفرق في الخفاء: فهرس geography سيتعامل بشكل صحيح مع الاستعلامات التي تغطي القطبين أو خط التاريخ الدولي، بينما لن يفعل فهرس geometry ذلك.

إليك استعلامًا لإيجاد جميع محطات المترو الواقعة ضمن 500 متر من مبنى Empire State.

WITH empire_state_building AS (
  SELECT 'POINT(-73.98501 40.74812)'::geography AS geog
)
SELECT name,
  ST_Distance(esb.geog, ss.geog) AS distance,
  degrees(ST_Azimuth(esb.geog, ss.geog)) AS direction
FROM nyc_subway_stations_geog ss,
     empire_state_building esb
WHERE ST_DWithin(ss.geog, esb.geog, 500);

لا يوجد سوى عدد قليل من الدوال الأصلية للنوع geography:

  • ST_AsText(geography) تُعيد text

  • ST_GeographyFromText(text) تُعيد geography

  • ST_AsBinary(geography) تُعيد bytea

  • ST_GeogFromWKB(bytea) تُعيد geography

  • ST_AsSVG(geography) تُعيد text

  • ST_AsGML(geography) تُعيد text

  • ST_AsKML(geography) تُعيد text

  • ST_AsGeoJson(geography) تُعيد text

  • ST_Distance(geography, geography) تُعيد double

  • ST_DWithin(geography, geography, float8) تُعيد boolean

  • ST_Area(geography) تُعيد double

  • ST_Length(geography) تُعيد double

  • ST_Covers(geography, geography) تُعيد boolean

  • ST_CoveredBy(geography, geography) تُعيد boolean

  • ST_Intersects(geography, geography) تُعيد boolean

  • ST_Buffer(geography, float8) تُعيد geography [1]

  • ST_Intersection(geography, geography) تُعيد geography [1]

18.2. إنشاء جدول Geography

تعليمات SQL لإنشاء جدول جديد يحتوي على عمود geography مشابهة كثيرًا لتلك الخاصة بإنشاء جدول geometry. غير أن geography تتيح تحديد نوع الكائن مباشرةً عند إنشاء الجدول. على سبيل المثال:

CREATE TABLE airports (
    code VARCHAR(3),
    geog GEOGRAPHY(Point)
  );

INSERT INTO airports
  VALUES ('LAX', 'POINT(-118.4079 33.9434)');
INSERT INTO airports
  VALUES ('CDG', 'POINT(2.5559 49.0083)');
INSERT INTO airports
  VALUES ('KEF', 'POINT(-22.6056 63.9850)');

في تعريف الجدول، يحدد GEOGRAPHY(Point) نوع بيانات المطارات لدينا على أنها نقاط. لا تُسجَّل حقول geography الجديدة في العرض (view) geometry_columns، بل تُسجَّل في عرض (view) يُسمى geography_columns.

SELECT * FROM geography_columns;
          f_table_name    | f_geography_column | srid |   type
--------------------------+--------------------+------+----------
 nyc_subway_stations_geog | geog               |    0 | Geometry
 airports                 | geog               | 4326 | Point

ملاحظة

حُذفت بعض الأعمدة من المخرجات أعلاه.

18.3. التحويل إلى Geometry

مع أن الدوال الأساسية لأنواع geography قادرة على تلبية كثير من حالات الاستخدام، إلا أنك قد تحتاج أحيانًا إلى دوال أخرى لا يدعمها إلا النوع geometry. لحسن الحظ، يمكنك تحويل الكائنات ذهابًا وإيابًا بين geography و geometry.

اصطلاح صياغة PostgreSQL لتحويل النوع (casting) هو إلحاق ::typename بنهاية القيمة التي تريد تحويلها. وعليه فإن 2::text ستحوّل الرقم اثنين إلى السلسلة النصية '2'. و 'POINT(0 0)'::geometry ستحوّل التمثيل النصي للنقطة إلى نقطة geometry.

الدالة ST_X(point) لا تدعم إلا النوع geometry. فكيف يمكننا قراءة الإحداثي X من بيانات geography لدينا؟

SELECT code, ST_X(geog::geometry) AS longitude FROM airports;
 code | longitude
------+-----------
 LAX  | -118.4079
 CDG  |    2.5559
 KEF  |  -21.8628

بإلحاق ::geometry بقيمة geography، نحوّل الكائن إلى geometry ذي SRID يساوي 4326. ومن هناك يمكننا استخدام ما نشاء من دوال geometry. لكن تذكّر: بما أن الكائن أصبح geometry، فستُفسَّر الإحداثيات على أنها إحداثيات ديكارتية لا كروية.

18.4. لماذا نستخدم Geography (أو لا نستخدمها)

الإحداثيات الجغرافية مقبولة عالميًا، فالجميع يفهم معنى خط العرض/خط الطول، لكن قلة قليلة من الناس تفهم معنى إحداثيات UTM. فلماذا لا نستخدم geography دائمًا؟

  • أولًا، كما ذُكر سابقًا، فإن عدد الدوال المتاحة (حاليًا) التي تدعم النوع geography مباشرةً أقل بكثير. وقد تقضي وقتًا طويلًا في الالتفاف على قيود النوع geography.

  • ثانيًا، الحسابات على الكرة أكثر كلفة حسابيًا بكثير من الحسابات الديكارتية. على سبيل المثال، تتضمن الصيغة الديكارتية للمسافة (فيثاغورس) استدعاءً واحدًا للدالة sqrt()، بينما تتضمن الصيغة الكروية للمسافة (Haversine) استدعاءين للدالة sqrt()، واستدعاءً للدالة arctan()، وأربعة استدعاءات للدالة sin()، واستدعاءين للدالة cos(). الدوال المثلثية مكلفة جدًا، والحسابات الكروية تتضمن الكثير منها.

الخلاصة؟

إذا كانت بياناتك متقاربة جغرافيًا (ضمن ولاية أو مقاطعة أو مدينة)، فاستخدم النوع geometry مع إسقاط ديكارتي يناسب بياناتك. راجع موقع http://epsg.io واكتب اسم منطقتك للحصول على مجموعة من الأنظمة المرجعية الممكنة.

إذا كنت تحتاج إلى قياس المسافات في مجموعة بيانات متباعدة جغرافيًا (تغطي جزءًا كبيرًا من العالم)، فاستخدم النوع geography. فالتعقيد الذي توفّره على تطبيقك بالعمل مع geography سيعوّض أي مشكلات في الأداء. كما أن التحويل إلى geometry يمكن أن يعوّض معظم القيود الوظيفية.

18.5. قائمة الدوال

ST_Distance(geometry, geometry): بالنسبة للنوع geometry، تُعيد أقل مسافة ديكارتية ثنائية الأبعاد (بناءً على النظام المرجعي المكاني) بين شكلين هندسيين بوحدات الإسقاط. وبالنسبة للنوع geography، تُعيد افتراضيًا أقل مسافة على الشكل الإهليلجي بين كائني geography بالأمتار.

ST_GeographyFromText(text): تُعيد قيمة geography محددة من تمثيل Well-Known Text (WKT) أو تمثيله الموسّع.

ST_Transform(geometry, srid): تُعيد شكلًا هندسيًا جديدًا بعد تحويل إحداثياته إلى SRID المشار إليه بالمعامل العددي الصحيح.

ST_X(point): تُعيد الإحداثي X للنقطة، أو NULL إن لم يكن متاحًا. يجب أن يكون المُدخل نقطة.

ST_Azimuth(geography_A, geography_B): تُعيد الاتجاه من A إلى B بالراديان.

ST_DWithin(geography_A, geography_B, R): تُعيد true إذا كان A ضمن R متر من B.

الحواشي