Changes between Initial Version and Version 1 of AdvancedTopics


Ignore:
Timestamp:
06/15/26 15:33:21 (3 days ago)
Author:
231102
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedTopics

    v1 v1  
     1= Напредна тема: ГИС (просторни податоци со PostGIS) =
     2
     3Како напредна тема инфраструктурниот модул (`or_`) е надграден во вистински географско-информациски (ГИС) потсистем преку '''PostGIS'''. Наместо објектите да чуваат „простор" како текст, тие сега чуваат реална геометрија со SRID, индексирана со GiST, и заштитена со просторни тригери.
     4
     5[attachment:14_gis.sql 14_gis.sql]
     6
     7
     8== Мотивација и опфат ==
     9
     10Просторното мапирање на објектите (згради, катови, простории, точки на интерес) е централно за инфраструктурниот модул. За тоа да носи реална вредност — содржаност, растојание, површина, спречување преклопување — просторот мора да биде разбирлив за базата, а не само текстуален запис. Надградбата опфаќа:
     11
     12 * миграција на геометријата на `or_mapa_objekt` од текст во `geometry(Geometry, 4326)`
     13 * енумерација `mapa_geom_tip` изведен автоматски од самата геометрија
     14 * GiST просторен индекс
     15 * 2 функции, 3 тригери и 1 процедура со просторна логика
     16 * хиерархиска и просторна валидација (кампус → зграда → кат → просторија)
     17
     18
     19== Дизајнерски одлуки ==
     20
     21=== 1. Отстранет `or_tip_mapa_objekt` ===
     22
     23Табелата `or_tip_mapa_objekt` беше замислена да ги чува геометриските типови (POINT, LINESTRING, POLYGON, MULTIPOLYGON). Бидејќи PostGIS веќе го знае типот на секоја геометрија преку `GeometryType(geom)`, одделен шифрарник со надворешен клуч е чист вишок. Затоа табелата, колоната `map_object_type_id` и ограничувањето `fk_mapa_obj_tip` се отстранети. (Семантичката класификација на објектите — зграда, просторија — и онака живее во `or_tip_objekt_organizacija`.)
     24
     25=== 2. Текст → `geometry(Geometry, 4326)` ===
     26
     27Колоната `spatial_tekst text` е заменета со `geom geometry(Geometry, 4326)`. SRID '''4326''' е WGS84 (географска должина/ширина во степени). Типот `Geometry` (генерички) дозволува различни видови геометрија во иста табела — полигони за отпечатоци и точки за локации.
     28
     29=== 3. Изведен енум `mapa_geom_tip` (преку тригер, не GENERATED) ===
     30
     31Типот на геометријата се изложува преку енум `mapa_geom_tip`, но вредноста '''не се внесува рачно''' — се изведува автоматски од геометријата, па никогаш не може да се разликува од податокот. Првично беше пробана `GENERATED` колона со израз `GeometryType(geom)::mapa_geom_tip`, но PostGIS го одби бидејќи изразот не е `IMMUTABLE`:
     32
     33{{{
     34ERROR:  generation expression is not immutable
     35}}}
     36
     37Затоа решението е `BEFORE` тригер (`trg_postavi_geom_tip`) кој ја поставува вредноста при секој внес/измена на геометријата — иста гаранција, без барањето за имутабилност.
     38
     39
     40== Надградба на шемата ==
     41
     42Енумот мора да постои пред табелата што го користи:
     43
     44{{{
     45CREATE TYPE mapa_geom_tip AS ENUM ('POINT', 'LINESTRING', 'POLYGON', 'MULTIPOLYGON');
     46}}}
     47
     48Структурата на `or_mapa_objekt` по надградбата:
     49
     50{{{
     51id                     integer  (identity, PRIMARY KEY)
     52geom                   geometry(Geometry, 4326)   -- вистинска геометрија (WGS84)
     53geom_tip               mapa_geom_tip              -- изведен од geom преку тригер
     54kapacitet              integer                    -- пресметан од површина
     55parent_mapa_objekt_id  integer → or_mapa_objekt(id)  -- просторна хиерархија
     56}}}
     57
     58 * '''geom''' — отпечаток (полигон) или локација (точка) во SRID 4326.
     59 * '''geom_tip''' — енум синхронизиран со геометријата (преку тригер 8.1).
     60 * '''kapacitet''' — се полни од површината преку процедурата.
     61 * '''parent_mapa_objekt_id''' — самореференца за дрвото кампус → зграда → кат → просторија.
     62
     63
     64== Функции ==
     65
     66Функциите враќаат вредност и се вградуваат директно во прашалник, поглед или друга рутина.
     67
     68=== 1. fn_povrshina_m2 ===
     69
     70{{{
     71CREATE OR REPLACE FUNCTION fn_povrshina_m2(p_mapa_objekt_id integer)
     72RETURNS numeric
     73LANGUAGE sql
     74STABLE
     75AS $$
     76    SELECT ROUND(ST_Area(geom::geography)::numeric, 2)
     77    FROM or_mapa_objekt
     78    WHERE id = p_mapa_objekt_id;
     79$$;
     80}}}
     81
     82Функцијата `fn_povrshina_m2` ја враќа површината на објектот во '''квадратни метри'''. Бидејќи SRID 4326 е во степени, геометријата се кастира во `geography` за да се добие геодезиска површина во метри (`ST_Area` врз чиста geometry би дал степени²). За објекти што немаат површина (POINT, LINESTRING) враќа 0. Реализирана е како функција бидејќи враќа единечна вредност што се вградува во прашалници и во процедурата за капацитет.
     83
     84=== 2. fn_najblisku_objekti ===
     85
     86{{{
     87CREATE OR REPLACE FUNCTION fn_najblisku_objekti(
     88    p_lon      double precision,
     89    p_lat      double precision,
     90    p_k        integer        DEFAULT 5,
     91    p_geom_tip mapa_geom_tip  DEFAULT NULL
     92)
     93RETURNS TABLE (mapa_objekt_id integer, tip mapa_geom_tip, rastojanie_m numeric)
     94LANGUAGE sql
     95STABLE
     96AS $$
     97    SELECT o.id,
     98           o.geom_tip,
     99           ROUND(ST_Distance(
     100                     o.geom::geography,
     101                     ST_SetSRID(ST_MakePoint(p_lon, p_lat), 4326)::geography
     102                 )::numeric, 2)
     103    FROM or_mapa_objekt o
     104    WHERE p_geom_tip IS NULL OR o.geom_tip = p_geom_tip
     105    ORDER BY o.geom <-> ST_SetSRID(ST_MakePoint(p_lon, p_lat), 4326)
     106    LIMIT p_k;
     107$$;
     108}}}
     109
     110Функцијата `fn_najblisku_objekti` ги враќа `k`-те најблиски објекти до дадена точка (KNN — k-nearest neighbours), со опционо филтрирање по геометриски тип. Подредувањето се прави преку GiST операторот `<->` врз geometry (брзо и индексирано), додека прикажаното растојание се пресметува преку `geography` за точни метри. На обем на кампус планарното подредување се совпаѓа со геодезиското, а индексот останува искористен. Враќа множество редови, па е функција (table-valued) што се користи со `SELECT * FROM fn_najblisku_objekti(...)`.
     111
     112
     113== Тригери ==
     114
     115Тригерите гарантираат дека правилото важи без оглед кој пишува во табелата — апликација, процедура или рачен INSERT.
     116
     117=== 1. trg_postavi_geom_tip ===
     118
     119{{{
     120CREATE OR REPLACE FUNCTION trgf_postavi_geom_tip()
     121RETURNS trigger
     122LANGUAGE plpgsql
     123AS $$
     124BEGIN
     125    NEW.geom_tip := GeometryType(NEW.geom)::mapa_geom_tip;
     126    RETURN NEW;
     127END;
     128$$;
     129
     130CREATE TRIGGER trg_postavi_geom_tip
     131    BEFORE INSERT OR UPDATE OF geom ON or_mapa_objekt
     132    FOR EACH ROW
     133    EXECUTE FUNCTION trgf_postavi_geom_tip();
     134}}}
     135
     136Тригерот `trg_postavi_geom_tip` го изведува енумот `geom_tip` од самата геометрија при секој внес или измена на `geom`. Тоа е замена за `GENERATED` колона (чиј израз не е IMMUTABLE), при што `geom_tip` секогаш останува во синхрон со податокот. Како бонус, ако геометријата е неподдржан тип (нпр. `GEOMETRYCOLLECTION`), кастот во енумот фрла грешка и внесот е одбиен.
     137
     138=== 2. trg_geom_vo_roditel ===
     139
     140{{{
     141CREATE OR REPLACE FUNCTION trgf_geom_vo_roditel()
     142RETURNS trigger
     143LANGUAGE plpgsql
     144AS $$
     145DECLARE
     146    v_parent geometry;
     147BEGIN
     148    IF NEW.parent_mapa_objekt_id IS NULL THEN
     149        RETURN NEW;   -- врвен објект (кампус) — нема родител
     150    END IF;
     151
     152    SELECT geom INTO v_parent
     153    FROM or_mapa_objekt
     154    WHERE id = NEW.parent_mapa_objekt_id;
     155
     156    IF v_parent IS NULL THEN
     157        RETURN NEW;
     158    END IF;
     159
     160    IF NOT ST_CoveredBy(NEW.geom, v_parent) THEN
     161        RAISE EXCEPTION 'Геометријата мора да лежи во родителскиот објект % (ST_CoveredBy = false)',
     162            NEW.parent_mapa_objekt_id;
     163    END IF;
     164
     165    RETURN NEW;
     166END;
     167$$;
     168
     169CREATE TRIGGER trg_geom_vo_roditel
     170    BEFORE INSERT OR UPDATE OF geom, parent_mapa_objekt_id ON or_mapa_objekt
     171    FOR EACH ROW
     172    EXECUTE FUNCTION trgf_geom_vo_roditel();
     173}}}
     174
     175Тригерот `trg_geom_vo_roditel` го спроведува просторниот интегритет на хиерархијата: геометријата на детето мора да лежи во геометријата на родителот (`ST_CoveredBy`). Реализиран е како тригер, а не `CHECK`, бидејќи правилото гледа во '''друг ред''' (родителот), што `CHECK` не дозволува. Се користи `ST_CoveredBy` наместо `ST_Within` за да се дозволи допир по раб (нпр. просторија до надворешниот ѕид на катот).
     176
     177=== 3. trg_bez_preklopuvanje ===
     178
     179{{{
     180CREATE OR REPLACE FUNCTION trgf_bez_preklopuvanje()
     181RETURNS trigger
     182LANGUAGE plpgsql
     183AS $$
     184BEGIN
     185    IF NEW.parent_mapa_objekt_id IS NULL THEN
     186        RETURN NEW;
     187    END IF;
     188
     189    IF GeometryType(NEW.geom) NOT IN ('POLYGON', 'MULTIPOLYGON') THEN
     190        RETURN NEW;   -- точки/линии не се проверуваат
     191    END IF;
     192
     193    IF EXISTS (
     194        SELECT 1
     195        FROM or_mapa_objekt o
     196        WHERE o.parent_mapa_objekt_id = NEW.parent_mapa_objekt_id
     197          AND o.id <> NEW.id
     198          AND GeometryType(o.geom) IN ('POLYGON', 'MULTIPOLYGON')
     199          AND ST_Intersects(o.geom, NEW.geom)
     200          AND NOT ST_Touches(o.geom, NEW.geom)
     201    ) THEN
     202        RAISE EXCEPTION 'Полигонот се преклопува со постоечки соседен објект под родителот %',
     203            NEW.parent_mapa_objekt_id;
     204    END IF;
     205
     206    RETURN NEW;
     207END;
     208$$;
     209
     210CREATE TRIGGER trg_bez_preklopuvanje
     211    BEFORE INSERT OR UPDATE OF geom, parent_mapa_objekt_id ON or_mapa_objekt
     212    FOR EACH ROW
     213    EXECUTE FUNCTION trgf_bez_preklopuvanje();
     214}}}
     215
     216Тригерот `trg_bez_preklopuvanje` спречува два полигони под ист родител да се преклопуваат. Комбинацијата `ST_Intersects AND NOT ST_Touches` го издвојува '''вистинското преклопување на внатрешности''' од дозволениот случај кога две соседни простории делат само ѕид (раб). Точките и линиите не се проверуваат. Тоа е круцијално за точни просторни распореди без преклопување на простории.
     217
     218
     219== Процедура ==
     220
     221=== 1. pr_dodeli_kapacitet_od_povrshina ===
     222
     223{{{
     224CREATE OR REPLACE PROCEDURE pr_dodeli_kapacitet_od_povrshina(p_m2_po_student numeric)
     225LANGUAGE plpgsql
     226AS $$
     227DECLARE
     228    r       RECORD;
     229    v_count integer := 0;
     230BEGIN
     231    IF p_m2_po_student <= 0 THEN
     232        RAISE EXCEPTION 'м² по студент мора да биде позитивно (добиено: %)', p_m2_po_student;
     233    END IF;
     234
     235    FOR r IN
     236        SELECT o.id, ST_Area(o.geom::geography) AS povrsina
     237        FROM or_mapa_objekt o
     238        WHERE GeometryType(o.geom) IN ('POLYGON', 'MULTIPOLYGON')
     239          AND NOT EXISTS (SELECT 1 FROM or_mapa_objekt c
     240                          WHERE c.parent_mapa_objekt_id = o.id)
     241    LOOP
     242        UPDATE or_mapa_objekt
     243        SET kapacitet = floor(r.povrsina / p_m2_po_student)
     244        WHERE id = r.id;
     245        v_count := v_count + 1;
     246    END LOOP;
     247
     248    RAISE NOTICE 'Поставен капацитет за % простории.', v_count;
     249END;
     250$$;
     251}}}
     252
     253Процедурата `pr_dodeli_kapacitet_od_povrshina` за сите „лист" полигони (простории без подобјекти) поставува `kapacitet = floor(површина_м² / m2_po_student)`. Реализирана е како процедура бидејќи е batch DML операција врз повеќе редови без повратна вредност за прашалник. Капацитет се пресметува само за крајните простории, не за катови/згради што ги содржат.
     254
     255
     256== Тест податоци и верификација ==
     257
     258За тестирање е внесена мала вгнездена хиерархија (схематски квадрати во WGS84, наменети да ја докажат логиката — содржаност, преклопување, KNN — а не реални димензии): кампус → зграда → кат → 2 простории + 1 точка на интерес.
     259
     260'''Преглед на објектите (изведен тип и површина):'''
     261
     262{{{
     263 id | geom_tip | parent_mapa_objekt_id |    m2
     264----+----------+-----------------------+-----------
     265  1 | POLYGON  |                       | 920179.37
     266  2 | POLYGON  |                     1 | 147230.98
     267  3 | POLYGON  |                     2 | 147230.98
     268  4 | POLYGON  |                     3 |  73615.49
     269  5 | POLYGON  |                     3 |  73615.49
     270  6 | POINT    |                     2 |      0.00
     271}}}
     272
     273`geom_tip` е точно изведен (POLYGON/POINT), а `fn_povrshina_m2` враќа метри (точката има 0).
     274
     275'''Капацитет од површина''' (`CALL pr_dodeli_kapacitet_od_povrshina(2.0);`) — се ажурираат само листовите (простории 4 и 5):
     276
     277{{{
     278 id | geom_tip | kapacitet |    m2
     279----+----------+-----------+----------
     280  4 | POLYGON  |     36807 | 73615.49
     281  5 | POLYGON  |     36807 | 73615.49
     282}}}
     283
     284'''Најблиски објекти''' од надворешна точка (`fn_najblisku_objekti(21.42, 42.02, 3)`) — растојание во метри, растечки:
     285
     286{{{
     287 mapa_objekt_id |   tip   | rastojanie_m
     288----------------+---------+--------------
     289              1 | POLYGON |      1385.58
     290              2 | POLYGON |      1939.84
     291              5 | POLYGON |      1939.84
     292}}}
     293
     294'''GeoJSON''' (`ST_AsGeoJSON(geom)`) — доказ дека податокот е реално просторен и подготвен за рендерирање на мапа:
     295
     296{{{
     297{"type":"Point","coordinates":[21.405,42.005]}
     298{"type":"Polygon","coordinates":[[[21.402,42.002],[21.406,42.002],...]]}
     299}}}
     300
     301'''Докази дека тригерите работат:'''
     302
     303 * Просторија со координати '''надвор''' од катот → одбиена од `trg_geom_vo_roditel`:
     304{{{
     305ERROR:  Геометријата мора да лежи во родителскиот објект 1 (ST_CoveredBy = false)
     306}}}
     307 * Две простории што делат ѕид (раб) → '''дозволени'''; просторија што навлегува во соседна → одбиена од `trg_bez_preklopuvanje`:
     308{{{
     309ERROR:  Полигонот се преклопува со постоечки соседен објект под родителот 3
     310}}}
     311 * Неподдржан геометриски тип (`GEOMETRYCOLLECTION`) → одбиен од кастот во `trg_postavi_geom_tip`:
     312{{{
     313ERROR:  invalid input value for enum mapa_geom_tip: "GEOMETRYCOLLECTION"
     314}}}
     315