Changes between Version 1 and Version 2 of AdvancedTopics


Ignore:
Timestamp:
05/20/26 12:23:50 (6 days ago)
Author:
231044
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedTopics

    v1 v2  
    1 = Advanced Topic - GIS
     1= Напредна тема: Имплементација на PostGIS =
     2
     3'''Проект:''' SBAirlines [[BR]]
     4'''Автори:''' Јован Величкоски 231044, Сандра Чавдарова 231026, Филип Лазаревски 231022
     5
     6----
     7
     8== 1. Вовед ==
     9
     10PostGIS е екстензија за PostgreSQL која ѝ овозможува на базата на податоци да складира, индексира и обработува геопросторни (geospatial) податоци. Со неа, базата може да работи со точки, линии, полигони и други геометриски објекти како native типови на податоци, и да извршува просторни прашалници (пресметка на растојание, проверка дали една геометрија се содржи во друга, наоѓање најблиски објекти итн.) директно во SQL.
     11
     12За овој проект, PostGIS претставува природен избор бидејќи доменот на авиокомпанија е инхерентно геопросторен — секој аеродром е точка на површината на Земјата, секој лет е траектoрија помеѓу две точки, а голем дел од оперативните и аналитичките прашалници на авиокомпанијата (растојанија, рути, најблиски аеродроми, локација на авион во моментот) се суштински просторни.
     13
     14----
     15
     16== 2. Зошто го избравме овој топик? ==
     17
     18Постојаат повеќе причини зошто го избравме PostGIS како напредна тема за нашиот проект:
     19
     20'''Природна интеграција со постоечкиот модел.''' Нашиот проект веќе содржи табели каде што геопросторната природа е очигледна (Airport, Flight, !ScheduledFlight). До оваа фаза, тие беа моделирани со текстуални имиња и кодови, но без вистинска географска информација. Воведувањето на PostGIS не претставува „вештачко" додавање на функционалност — туку логично продолжение и обогатување на она што веќе постои.
     21
     22'''Замена на синтетички податоци со реални вредности.''' Во Фаза 2Б, колоната {{{ScheduledFlight.Distance}}} беше пополнета со псевдо-случајни вредности генерирани преку формулата {{{duration * 13 + noise}}}. Со PostGIS можеме да ја пресметаме вистинската географска дистанца помеѓу аеродромите (great-circle distance). Тоа автоматски прави повеќе постоечки делови од системот да станат „вистински" — на пример, тригерот {{{AwardMilePoints}}} од Фаза 4 веќе ги доделува миљните поени на патниците врз основа на реална географска дистанца.
     23
     24'''Демонстрација на просторно индексирање.''' PostGIS ги воведе GIST (Generalized Search Tree) индексите, кои овозможуваат брза просторна претрага врз милиони редици. Ова е природно продолжение на Фаза 3 (оптимизација) — освен B-tree индексите кои ги видовме таму, сега работиме и со просторни индекси.
     25
     26'''Визуелизација и интеграција со надворешни алатки.''' PostGIS овозможува директна интеграција со QGIS — индустриски стандарден GIS алат. Тоа ни овозможи визуелно да го прикажеме целиот мрежен систем на авиокомпанијата (мапа на сите аеродроми, рутна мрежа, локации на летови) што е невозможно со чист релациски модел.
     27
     28'''Реалистичен случај на употреба.''' Сите вистински авиокомпании во светот користат геопросторни бази на податоци за оперативни цели — следење на летови, барање на алтернативни аеродроми, координација со контрола на летање. Имплементацијата на PostGIS го прави нашиот проект многу поблиску до реален систем.
     29
     30----
     31
     32== 3. Каде е интегриран PostGIS? ==
     33
     34PostGIS се протега низ повеќе компоненти на постоечкиот проект:
     35
     36|| '''Компонента''' || '''Тип на интеграција''' ||
     37|| Табела {{{Airport}}} || Додадени се колоните {{{latitude}}}, {{{longitude}}}, {{{location}}} (geography Point) ||
     38|| Колона {{{Airport.location}}} || Просторен GIST индекс за брзи геопросторни прашалници ||
     39|| Табела {{{ScheduledFlight}}} || Колоната {{{distance}}} е ажурирана со пресметани реални дистанци ||
     40|| Тригер {{{SetDistance}}} || Автоматски пресметува дистанца при INSERT/UPDATE на !ScheduledFlight ||
     41|| Поглед {{{FlightRoutes}}} || Нов поглед кој ги претставува летовите како !LineString геометрии ||
     42|| Функција {{{FindNearestAirport}}} || Нова функција за наоѓање најблиски аеродроми ||
     43|| Функција {{{FlightAtTime}}} || Нова функција за пресметка на локација на лет во даден момент ||
     44|| Тригер {{{AwardMilePoints}}} (Фаза 4) || Сега ги пресметува миљните поени врз основа на реални растојанија ||
     45
     46----
     47
     48== 4. Технички предуслови ==
     49
     50Пред да може да се користи PostGIS, екстензијата мора да биде активирана во базата на податоци:
     51
     52{{{
     53#!sql
     54CREATE EXTENSION postgis;
     55}}}
     56
     57Оваа команда ги регистрира сите PostGIS типови на податоци ({{{geography}}}, {{{geometry}}}), функции ({{{ST_Distance}}}, {{{ST_MakePoint}}}, {{{ST_LineInterpolatePoint}}} итн.) и оператори (просторниот KNN оператор {{{<->}}}) во моменталната база.
     58
     59----
     60
     61== 5. Промени на постоечките табели ==
     62
     63=== 5.1 Додавање на колоните latitude и longitude ===
     64
     65Првиот чекор беше да се обогати табелата {{{Airport}}} со географски координати. Овие податоци ги имавме од оригиналниот CSV фајл ({{{airports.csv}}} од !OpenFlights), но во Фаза 2Б беа отфрлени бидејќи тогаш немавме просторна функционалност.
     66
     67{{{
     68#!sql
     69ALTER TABLE airport ADD COLUMN latitude  numeric;
     70ALTER TABLE airport ADD COLUMN longitude numeric;
     71}}}
     72
     73Колоните потоа се пополнија со податоци од привремена табела {{{temp_airports}}}, спарувајќи по IATA код:
     74
     75{{{
     76#!sql
     77UPDATE airport a
     78SET latitude  = ta.latitude::numeric,
     79    longitude = ta.longitude::numeric
     80FROM (
     81    SELECT DISTINCT ON (iata) iata, latitude, longitude
     82    FROM temp_airports
     83    WHERE latitude  NOT IN ('\N','')
     84      AND longitude NOT IN ('\N','')
     85    ORDER BY iata
     86) ta
     87WHERE a.code = ta.iata;
     88}}}
     89
     90Овие колони служат како „сурова" форма на координатите — лесни за читање и експортирање, но не директно искористливи во просторни прашалници.
     91
     92=== 5.2 Додавање на колоната location (geography Point) ===
     93
     94Главната просторна колона е {{{location}}}, која ги обединува latitude и longitude во една PostGIS геометрија:
     95
     96{{{
     97#!sql
     98ALTER TABLE airport
     99ADD COLUMN location geography(Point, 4326);
     100
     101UPDATE airport
     102SET location = ST_MakePoint(longitude, latitude)::geography;
     103}}}
     104
     105Неколку важни детали:
     106
     107 * '''Тип {{{geography(Point, 4326)}}}.''' PostGIS нуди два главни типа на просторни податоци: {{{geometry}}} (рамна, евклидска геометрија) и {{{geography}}} (вистинска сферна геометрија на Земјата). За авиокомпаниски проект, {{{geography}}} е правилниот избор бидејќи летовите се на голема скала и кривината на Земјата е значајна — растојанијата мора да се мерат како great-circle distances, а не како евклидски растојанија.
     108 * '''SRID 4326.''' Ова е стандардот WGS 84 (World Geodetic System 1984), истиот референтен систем кој го користат GPS уредите. Со ова, координатите од нашиот CSV (кои се во WGS 84) се компатибилни со колоната без потреба од репроекција.
     109 * '''Редослед на параметри: {{{ST_MakePoint(longitude, latitude)}}}.''' PostGIS следи XY-конвенција, што значи дека longitude е X (надолжна, „хоризонтална" оска) и latitude е Y. Замена на овие два параметра е честа грешка која води до точки во погрешен дел на светот.
     110
     111=== 5.3 Просторен GIST индекс ===
     112
     113За да може PostGIS брзо да изврши просторни прашалници врз милиони редици, на колоната {{{location}}} се додаде GIST индекс:
     114
     115{{{
     116#!sql
     117CREATE INDEX idx_airport_location ON airport USING GIST (location);
     118}}}
     119
     120За разлика од B-tree индексите (кои се користат за подредени скаларни вредности — броеви, текстови, датуми), GIST индексите се специјализирани за повеќедимензионални податоци. Тие овозможуваат:
     121
     122 * '''Range queries во просторот''' — „пронајди ги сите аеродроми во радиус од 500 km околу точката X"
     123 * '''Nearest-neighbor queries''' — „пронајди ги најблиските 10 аеродроми од дадена точка", преку K-nearest-neighbor операторот {{{<->}}}
     124 * '''Intersection queries''' — „пронајди ги сите рути кои поминуваат низ дадена област"
     125
     126Без GIST индекс, секоја таква претрага би била sequential scan низ целата табела.
     127
     128----
     129
     130== 6. Поглед FlightRoutes ==
     131
     132Создаден е нов поглед кој ги претставува летовите како геометриски линии на мапата:
     133
     134{{{
     135#!sql
     136CREATE VIEW FlightRoutes (FlightID, FlightNumber, Departure, Arrival,
     137    Origin, Destination, Route, Distance) AS
     138SELECT flight.id, flight.flightnumber,
     139       flight.departure, flight.arrival,
     140       ad.code, aa.code,
     141       ST_MakeLine(ad.location::geometry, aa.location::geometry),
     142       scheduledflight.distance
     143FROM flight
     144JOIN gate     AS ga ON flight.actualgatearrivalid   = ga.id
     145JOIN gate     AS gd ON flight.actualgatedepartureid = gd.id
     146JOIN terminal AS ta ON ta.id = ga.terminalid
     147JOIN terminal AS td ON td.id = gd.terminalid
     148JOIN airport  AS aa ON aa.id = ta.airportid
     149JOIN airport  AS ad ON ad.id = td.airportid
     150JOIN scheduledflight ON flight.scheduleid = scheduledflight.id
     151WHERE aa.location IS NOT NULL AND ad.location IS NOT NULL;
     152}}}
     153
     154'''Како функционира:'''
     155
     156 * Функцијата {{{ST_MakeLine(departure_point, arrival_point)}}} гради {{{LineString}}} геометрија — права линија помеѓу две точки во геометрискиот простор.
     157 * Низ ланецот од JOIN-ови, погледот ги доведува аеродромите за полетување и слетување на секој лет (преку gate → terminal → airport).
     158 * Резултатот е една редица по лет, со полно име, временски печат, кодови на полетниот и слетниот аеродром, реална географска линија помеѓу нив и пресметана дистанца.
     159
     160'''Случаи на употреба:'''
     161
     162 * '''Визуелизација во QGIS''' — погледот може да се вчита како просторен слој и да прикаже сите рути на мапа.
     163 * '''Мрежна анализа''' — кои аеродроми се најпрометни, кои рути се најфреквентни, која е географската покриеност на авиокомпанијата.
     164 * '''Просторни прашалници''' — на пример, „кои летови поминуваат во радиус од 200 km околу одредена точка" може да се изврши со {{{ST_DWithin}}} врз колоната {{{Route}}}.
     165
     166=== 6.1 Скриншоти ===
     167
     168'''Сите аеродроми на светот:'''[[BR]]
     169[[Image(Screenshots/World-Airports.png)]]
     170
     171'''Аеродроми на Балканот:'''[[BR]]
     172[[Image(Screenshots/Balkan.png)]]
     173
     174'''Рути од Скопје:'''[[BR]]
     175[[Image(Screenshots/RoutesFromSkopje.png)]]
     176
     177'''Рути до Newark (EWR):'''[[BR]]
     178[[Image(Screenshots/RoutesToNewark.png)]]
     179
     180----
     181
     182== 7. Ажурирање на дистанците во ScheduledFlight ==
     183
     184Една од најважните придобивки на PostGIS во овој проект е замена на лажните, синтетички дистанци со вистински географски вредности.
     185
     186{{{
     187#!sql
     188UPDATE scheduledflight AS sf
     189SET distance = GREATEST(1, ROUND(ST_Distance(dep.location, arr.location) / 1000)::int)
     190FROM slot AS ds
     191JOIN gate     AS dg ON ds.gateid     = dg.id
     192JOIN terminal AS dt ON dg.terminalid = dt.id
     193JOIN airport  AS dep ON dep.id       = dt.airportid,
     194     slot AS asl
     195JOIN gate     AS ag ON asl.gateid    = ag.id
     196JOIN terminal AS at ON at.id         = ag.terminalid
     197JOIN airport  AS arr ON arr.id       = at.airportid
     198WHERE sf.departure = ds.id
     199  AND sf.arrival   = asl.id
     200  AND dep.location IS NOT NULL
     201  AND arr.location IS NOT NULL;
     202}}}
     203
     204'''Детали за пресметката:'''
     205
     206 * {{{ST_Distance(dep.location, arr.location)}}} враќа great-circle дистанца помеѓу две {{{geography}}} точки во метри.
     207 * Делењето со 1000 ја претвора во километри.
     208 * {{{ROUND(...)::int}}} ја кружи до цел број за да одговара на типот на колоната.
     209 * {{{GREATEST(1, ...)}}} гарантира минимална вредност од 1 km (поради CHECK ограничувањето {{{distance > 0}}}). Ова е важно за ретки случаи каде два аеродроми се толку близу што дистанцата заокружена на километри би била 0.
     210
     211'''Последица низ остатокот на системот:'''
     212
     213Постоечкиот тригер {{{AwardMilePoints}}} (од Фаза 4) ги пресметува миљните поени на патниците врз основа на {{{ScheduledFlight.Distance}}}, помножена со множител по класа (Economy: 1×, Business: 2×, First: 3×). По ова ажурирање, поените повеќе не се „измислени" — патник со First класа на лет JFK → LAX добива ~4000 km × 3 = 12,000 поени, што одговара на реално долг лет.
     214
     215----
     216
     217== 8. Тригер SetDistance (CalculateDistance) ==
     218
     219За да не мора рачно да се пресметува дистанцата секогаш кога ќе се додаде нов закажан лет, имплементиран е тригер кој автоматски ја пресметува:
     220
     221{{{
     222#!sql
     223CREATE OR REPLACE FUNCTION CalculateDistance()
     224RETURNS trigger AS $$
     225BEGIN
     226    SELECT GREATEST(1, ROUND(ST_Distance(dep.location, arr.location) / 1000)::int)
     227    INTO NEW.distance
     228    FROM slot AS ds
     229    JOIN gate     AS dg ON ds.gateid     = dg.id
     230    JOIN terminal AS dt ON dg.terminalid = dt.id
     231    JOIN airport  AS dep ON dep.id       = dt.airportid,
     232         slot AS asl
     233    JOIN gate     AS ag ON asl.gateid    = ag.id
     234    JOIN terminal AS at ON at.id         = ag.terminalid
     235    JOIN airport  AS arr ON arr.id       = at.airportid
     236    WHERE NEW.departure = ds.id
     237      AND NEW.arrival   = asl.id
     238      AND dep.location IS NOT NULL
     239      AND arr.location IS NOT NULL;
     240
     241    RETURN NEW;
     242END;
     243$$ LANGUAGE plpgsql;
     244
     245CREATE TRIGGER SetDistance
     246BEFORE INSERT OR UPDATE OF departure, arrival ON scheduledflight
     247FOR EACH ROW EXECUTE FUNCTION CalculateDistance();
     248}}}
     249
     250'''Карактеристики:'''
     251
     252 * '''{{{BEFORE INSERT OR UPDATE}}}''' — тригерот се извршува ''пред'' редицата да биде впишана/ажурирана. Тоа му овозможува да ја смени вредноста на {{{NEW.distance}}} пред таа да биде валидирана од CHECK ограничувањата.
     253 * '''{{{UPDATE OF departure, arrival}}}''' — тригерот се извршува само кога овие конкретни колони се менуваат. Доколку се ажурира некоја друга колона (на пример, {{{active}}}), тригерот не се активира — оптимизација која штеди непотребна работа.
     254 * '''{{{SELECT ... INTO NEW.distance}}}''' — наместо да враќа вредност на повикувачот, тригерот ја менува вредноста на {{{NEW}}} пред commit-ирање.
     255
     256'''Зошто тригер наместо generated column?''' PostgreSQL поддржува {{{GENERATED ALWAYS AS (...) STORED}}} колони, но тие не можат да користат subqueries или JOIN-ови (бидејќи мораат да бидат детерминистички и без странични ефекти). Бидејќи пресметката на дистанцата бара JOIN преку {{{slot → gate → terminal → airport}}}, мораше да се користи тригер.
     257
     258'''Случај на употреба:''' доколку во иднина се додадат нови закажани летови (нови дестинации, проширување на мрежата), нивната дистанца автоматски се пресметува правилно — администраторот не мора да внимава да ја внесе како посебен чекор.
     259
     260----
     261
     262== 9. Функција FindNearestAirport ==
     263
     264{{{
     265#!sql
     266CREATE OR REPLACE FUNCTION FindNearestAirport(
     267    longitude_P numeric,
     268    latitude_P  numeric,
     269    distanceKm  int
     270) RETURNS TABLE (
     271    AirportCode     char(3),
     272    AirportName     text,
     273    AirportLocation geography(Point, 4326),
     274    Distance_KM     numeric
     275) AS $$
     276BEGIN
     277    RETURN QUERY
     278    SELECT a.code, a.name, a.location,
     279           (ST_Distance(a.location, ST_MakePoint(longitude_P, latitude_P)::geography) / 1000)::numeric
     280    FROM airport AS a
     281    WHERE ST_DWithin(
     282        a.location,
     283        ST_MakePoint(longitude_P, latitude_P)::geography,
     284        distanceKm * 1000
     285    )
     286    ORDER BY a.location <-> ST_MakePoint(longitude_P, latitude_P)::geography;
     287END;
     288$$ LANGUAGE plpgsql;
     289}}}
     290
     291'''Како функционира:'''
     292
     293 * Прима три параметри: longitude и latitude на референтна точка, плус максимален радиус во километри.
     294 * Враќа табела на сите аеродроми во тој радиус, заедно со нивната локација и точното растојание.
     295 * Резултатите се сортирани по растојание — најблискиот аеродром е прв.
     296
     297'''Клучни PostGIS компоненти:'''
     298
     299 * '''{{{ST_DWithin(geog_a, geog_b, distance_meters)}}}''' — враќа TRUE доколку две географски точки се на растојание помало или еднакво на дадениот број метри. Ова е првиот филтер кој го „сече" пребарувањето.
     300 * '''Множењето {{{distanceKm * 1000}}}''' ги претвора километрите во метри (PostGIS интерно работи во метри за geography).
     301 * '''KNN оператор {{{<->}}}''' — посебен оператор за nearest-neighbor сортирање. За разлика од обичен {{{ORDER BY ST_Distance(...)}}}, овој оператор го користи GIST индексот директно и е значајно побрз при големи табели.
     302
     303'''Случаи на употреба:'''
     304
     305 * '''Препораки за алтернативни аеродроми''' — кога одреден лет се откажува, можеме да им предложиме на патниците блиски аеродроми (на пример, ако се откажува лет до LHR, можеме да предложиме LGW, STN, LTN).
     306 * '''Логистика на товар''' — наоѓање најблизок аеродром до магацин или дистрибутивен центар.
     307 * '''Кориснички интерфејс''' — „наоѓаш ли се близу аеродром?" функционалност за мобилна апликација.
     308
     309'''Демонстрација:''' наоѓање на најблиските аеродроми во радиус од 100 km околу Скопје (координати 21.62°E, 41.96°N):
     310
     311{{{
     312#!sql
     313SELECT * FROM FindNearestAirport(21.62, 41.96, 100);
     314}}}
     315
     316[[Image(Screenshots/NearestAirportsSkopje.png)]]
     317
     318----
     319
     320== 10. Функција FlightAtTime ==
     321
     322Оваа функција пресметува каде се наоѓа определен лет (естимативно) во даден временски момент — без потреба од посебна табела со логирани локации.
     323
     324{{{
     325#!sql
     326CREATE OR REPLACE FUNCTION FlightAtTime(
     327    FlightID_P int,
     328    Time_P     timestamp
     329) RETURNS TABLE (
     330    CurrentLocation geography,
     331    Route           geography
     332) AS $$
     333DECLARE
     334    DepartureTime     timestamp;
     335    ArrivalTime       timestamp;
     336    DepartureLocation geography;
     337    ArrivalLocation   geography;
     338    Fraction          numeric;
     339    CurrentPosition_V geography;
     340    Route_V           geography;
     341BEGIN
     342    SELECT flight.departure, flight.arrival, ad.location, aa.location
     343    INTO DepartureTime, ArrivalTime, DepartureLocation, ArrivalLocation
     344    FROM flight
     345    JOIN gate     AS ga ON flight.actualgatearrivalid   = ga.id
     346    JOIN gate     AS gd ON flight.actualgatedepartureid = gd.id
     347    JOIN terminal AS ta ON ta.id = ga.terminalid
     348    JOIN terminal AS td ON td.id = gd.terminalid
     349    JOIN airport  AS aa ON aa.id = ta.airportid
     350    JOIN airport  AS ad ON ad.id = td.airportid
     351    WHERE flight.id = FlightID_P;
     352
     353    IF Time_P <= DepartureTime THEN
     354        CurrentPosition_V := DepartureLocation;
     355    ELSIF Time_P >= ArrivalTime THEN
     356        CurrentPosition_V := ArrivalLocation;
     357    ELSE
     358        Fraction := EXTRACT(epoch FROM (Time_P - DepartureTime))
     359                  / EXTRACT(epoch FROM (ArrivalTime - DepartureTime));
     360
     361        CurrentPosition_V := ST_LineInterpolatePoint(
     362            ST_MakeLine(DepartureLocation::geometry, ArrivalLocation::geometry),
     363            Fraction
     364        )::geography;
     365    END IF;
     366
     367    Route_V := ST_MakeLine(
     368        DepartureLocation::geometry,
     369        ArrivalLocation::geometry
     370    )::geography;
     371
     372    RETURN QUERY SELECT CurrentPosition_V, Route_V;
     373END;
     374$$ LANGUAGE plpgsql;
     375}}}
     376
     377'''Како функционира:'''
     378
     379 1. '''Се извлекуваат деталите за летот''' — времето на полетување и слетување, и локациите на двата аеродрома преку познатиот ланец на JOIN-ови.
     380 2. '''Се определува моменталната локација според временскиот параметар:'''
     381   * Доколку времето е ''пред'' полетувањето, авионот сè уште е на полетниот аеродром.
     382   * Доколку времето е ''по'' слетувањето, авионот веќе пристигнал на слетниот аеродром.
     383   * Во спротивно, се пресметува {{{Fraction}}} — фракција помеѓу 0 и 1 која означува „колку процент од летот е изминат".
     384 3. '''Се користи {{{ST_LineInterpolatePoint}}}''' — функција која враќа точка на одредена фракција должина по една линија. Со fraction = 0.5, се добива средината на патот; со fraction = 0.25, една четвртина од него.
     385 4. '''Се враќа и моменталната локација и целата рута''' — корисно за визуализација (точка на мапа за авионот, плус линија која ја прикажува неговата траектoрија).
     386
     387'''Битна забелешка за реалност:'''
     388
     389Оваа функција користи '''линеарна интерполација по great-circle линија''' — претпоставува дека авионот лета со константна брзина по најкраткиот пат на сфера. Во вистинскиот свет:
     390
     391 * Авионите не летаат со константна брзина (имаат фази на качување, крстарење и спуштање).
     392 * Реалните рути често отстапуваат од идеалната great-circle линија поради воздушни кориди, временски услови, забранети воздушни простори.
     393 * Реалните системи за следење на летови (Flight Tracking) користат '''посебна табела за логирање на локации''' — на пример, ADS-B транспондерите на авионите емитуваат локација секои неколку секунди, а сите тие точки се чуваат во база.
     394
     395Бидејќи во овој проект немаме вистински авиони, импровизираме со пресметана локација. Во документацијата експлицитно споменуваме дека ова е демонстративен пристап, не продукциски модел.
     396
     397'''Случаи на употреба:'''
     398
     399 * '''Air Traffic Control визуелизација''' — приказ каде се сите авиони во моментот.
     400 * '''Информации за патници''' — „вашиот лет моментално е над Атлантскиот океан, на 70% од патот".
     401 * '''Анализа на оперативни инциденти''' — со повикување на {{{FlightAtTime(flight_id, incident_time)}}} се добива каде се наоѓал авионот кога настанал инцидентот.
     402
     403'''Демонстрација:'''
     404
     405[[Image(Screenshots/FlightAtDemo.png)]]
     406
     407----
     408
     409== 11. Визуелизација со QGIS ==
     410
     411Една од најголемите предности на PostGIS е природната интеграција со '''QGIS''' — индустриски стандарден отворен GIS алат. QGIS може директно да се поврзе со нашата PostGIS база и да ги визуелизира просторните слоеви во реално време.
     412
     413'''Процес на конекција:'''
     414
     415 1. Во QGIS се додава PostGIS конекција (Browser → !PostgreSQL → New Connection) со параметрите на базата.
     416 2. Сите табели и погледи со просторни колони (Airport, !FlightRoutes итн.) автоматски се препознаваат како слоеви.
     417 3. Со drag-and-drop се додаваат на мапата.
     418 4. Како подлога се користи !OpenStreetMap (преку XYZ Tiles), кој дава реален географски контекст.
     419
     420'''Што е визуелизирано:'''
     421
     422 * '''Слој на аеродроми''' — секој аеродром се прикажува како точка, со label-от поставен на IATA код.
     423 * '''Слој на рути''' — секоја линија претставува лет помеѓу два аеродроми. При зголемување на zoom, се гледа конкретниот географски пат.
     424 * '''Демонстрација на функциите''' — {{{FindNearestAirport}}} и {{{FlightAtTime}}} се повикуваат преку DB Manager → SQL Window, и нивните резултати се вчитуваат како нови слоеви.
     425
     426QGIS проектот е зачуван како {{{SBAirlines.qgz}}} и претставува дел од испораката на оваа фаза.
     427
     428----
     429
     430== 12. Краток преглед на новите компоненти ==
     431
     432|| '''Компонента''' || '''Тип''' || '''Намена''' ||
     433|| {{{airport.latitude}}}, {{{airport.longitude}}} || Нови колони || Сурови географски координати ||
     434|| {{{airport.location}}} || Нова колона || PostGIS геометриска точка (geography Point, 4326) ||
     435|| {{{idx_airport_location}}} || Просторен индекс || GIST индекс за брзи просторни прашалници ||
     436|| {{{FlightRoutes}}} || Нов поглед || Сите летови како !LineString геометрии ||
     437|| {{{ScheduledFlight.distance}}} || Ажурирана колона || Реални great-circle растојанија наместо синтетички ||
     438|| {{{CalculateDistance()}}} + {{{SetDistance}}} || Нов тригер || Автоматска пресметка на дистанца при INSERT/UPDATE ||
     439|| {{{FindNearestAirport(lon, lat, km)}}} || Нова функција || Враќа аеродроми во даден радиус, сортирани по близина ||
     440|| {{{FlightAtTime(id, time)}}} || Нова функција || Пресметува локација на лет во даден момент ||
     441
     442----
     443
     444== 13. Заклучок ==
     445
     446Интеграцијата на PostGIS во проектот SBAirlines не претставува површно додавање на функционалност, туку природна еволуција на моделот кон вистинскиот свет на авиокомпанијата. Со неа:
     447
     448 * '''Аеродромите имаат реални координати''', а не само имиња и кодови.
     449 * '''Растојанијата на летовите се вистински''', што влијае врз постоечките компоненти (миљни поени, оптимизација на флота, оперативни извештаи).
     450 * '''Се отвораат нови случаи на употреба''' кои се невозможни во чист релациски модел (наоѓање најблиски аеродроми, визуализација на мрежа, следење на локација).
     451 * '''Се демонстрира просторно индексирање''' — нов вид индекс кој не беше покриен во основните фази на проектот.
     452 * '''Базата на податоци станува извор на вистина''' за визуелни алатки како QGIS, кои директно ги читаат просторните слоеви.
     453
     454PostGIS ја претвора нашата база од „евиденција за авиокомпанија" во „геопросторна оперативна платформа" — токму она што го користат вистинските авиокомпании во светот.