| Version 15 (modified by , 26 hours ago) ( diff ) |
|---|
Advanced Concepts
Во нашиот проект користиме PostGIS бидејќи тој овозможува напредно геопросторлно моделирање и ефикасно филтрирање на податоците во реално време (како барање слободни такси возила во одреден радиус околу корисникот), што драстично го намалува бројот на обработени редови во базата и овозможува брзи локациски пресметки напредно од стандардниот релационен модел.
Во оваа фаза од проектот, стандардниот релационен модел е надограден со просторни функционалности преку екстензијата PostGIS. Дополнително, за следење на возилата е имплементирана Lambda архитектура (Hot/Cold складирање) со 4D просторно-временски траектории (LineStringZM).
Инсталација на PostGIS
За да се користи на Linux, екстензијата се инсталира во два брзи чекори:
Инсталација на пакетот на серверот преку терминал: sudo apt update && sudo apt install postgresql-15-postgis-3 (верзијата се прилагодува според инсталираниот PostgreSQL).
Активирање во самата база со извршување на командата: create extension postgis;
Иницијализација на PostGIS и конверзија во полигони (Geofencing)
Првиот чекор вклучува овозможување на PostGIS екстензијата и мигрирање на постоечките координати во geometry објекти. Дополнителна промена е отфрлањето на обичните радиуси за зоните на компаниите. Наместо тоа, користејќи ја функцијата ST_Buffer, кружните области се конвертирани во прецизни полигони со што се овозможува вистинско дигитално надградување. Креирани се и GiST (Generalized Search Tree) индекси кои се потребни за брзо пребарување на просторни податоци.
create extension if not exists postgis schema public;
alter table driver add column location geometry(Point, 4326);
update driver
set location = st_setsrid(st_makepoint(longitude, latitude), 4326);
alter table request add column start_location geometry(Point, 4326);
alter table request add column end_location geometry(Point, 4326);
update request
set start_location = st_setsrid(st_makepoint(start_longitude, start_latitude), 4326);
update request
set end_location = st_setsrid(st_makepoint(end_longitude, end_latitude), 4326);
alter table waypoints add column location geometry(Point, 4326);
update waypoints
set location = st_setsrid(st_makepoint(longitude, latitude), 4326);
alter table location add column location geometry(Point, 4326);
update location
set location = st_setsrid(st_makepoint(longitude, latitude), 4326);
alter table report add column location geometry(Point, 4326);
update report
set location = st_setsrid(st_makepoint(longitude, latitude), 4326);
alter table area add column coverage_polygon geometry(Polygon, 4326);
update area
set coverage_polygon = ST_Buffer(
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography,
radius
)::geometry;
create index idx_area_coverage_polygon on area using gist (coverage_polygon);
Просторно филтрирање на барања (Spatial Join)
За ефикасно поврзување на патниците со релевантните такси компании, креиран е нов поглед кој користи просторно спојување (Spatial Join). Преку функцијата ST_Contains, погледот проверува дали почетната локација на патникот (start_location) се наоѓа во рамките на полигонот на покриеност на компанијата (coverage_polygon). Овој пристап е многу поефикасен од пресметување на растојанија. Ова е оптимизацијата која ги подобрува перформансите на погледот vw_unassigned_requests спомнат во делот за QueryOptimization каде почетно беше решено со пагинација на барањата, а во случајот се земаат помал број на барања поради тоа што ќе се земат само барања кои се наоѓаат во одреден радиус.
create or replace view vw_company_available_requests as
select
r.id as request_id,
r.customer_user_id,
r.timestamp,
r.number_of_adult_passengers,
r.number_of_children,
r.status,
r.female_driver,
r.luggage,
r.luggage_count,
r.baby_seat_count,
ca.company_id
from company_area ca
join area a on ca.area_id = a.id
join request r on ST_Intersects(a.coverage_polygon, r.start_location)
and r.status = 'pending';
select * from vw_company_available_requests where company_id = 3;
Резултантно време на извршување:
Динамична пресметка на цена преку просторна дистанца
Креирана е нова функција calculate_price која ја пресметува цената на возењето. Таа користи ST_Distance со кастирање во geography за да ја добие точната воздушна дистанца во километри помеѓу почетната и крајната дестинација, по што го множи растојанието со соодветната тарифа на компанијата или фриленсерот. Тарифата може да биде цена за секоја минута возење или цена за секој изминат километар.
CREATE OR REPLACE FUNCTION calculate_price(request_id int4, id_company int4, id_area int4, freelance_driver_id int4)
RETURNS numeric(19,2)
LANGUAGE plpgsql
AS
$$
DECLARE
distance_km numeric;
price_per_km numeric;
price_per_min numeric;
BEGIN
SELECT
ST_Distance(
start_location::geography,
end_location::geography
) / 1000.0
INTO distance_km
FROM request
WHERE id = request_id;
IF distance_km IS NULL THEN
RAISE EXCEPTION 'Request % not found', request_id;
END IF;
SELECT value
INTO price_per_km
FROM pricinginfo p
JOIN company_area c on p.id=c.pricing_info_id
WHERE c.company_id=id_company and c.area_id=id_area and unit='kilometer'
LIMIT 1;
SELECT value
INTO price_per_km
FROM pricinginfo p
JOIN freelancedriver d on p.id=d.pricing_info_id
WHERE d.driver_user_id=freelance_driver_id and unit='kilometer'
LIMIT 1;
SELECT value
INTO price_per_min
FROM pricinginfo p
JOIN company_area c on p.id=c.pricing_info_id
WHERE c.company_id=id_company and c.area_id=id_area and unit='minute'
LIMIT 1;
SELECT value
INTO price_per_min
FROM pricinginfo p
JOIN freelancedriver d on p.id=d.pricing_info_id
WHERE d.driver_user_id=freelance_driver_id and unit='minute'
LIMIT 1;
IF price_per_km IS NULL and price_per_min IS NULL
THEN
RAISE EXCEPTION 'Pricing info not found';
end if;
IF price_per_km IS NOT NULL
THEN
RETURN ROUND((distance_km * price_per_km)::numeric, 2);
ELSE
RETURN ROUND((distance_km/40 * price_per_min * 60)::numeric, 2);
end if;
END;
$$;
Наоѓање на најблизок возач (K-Nearest Neighbors)
При креирањето на понуда од страна на диспечерот, системот автоматски го бара најблискиот слободен возач од таа компанија. За оваа цел се користи PostGIS операторот <->, кој го пресметува просторното растојание користејќи го GiST индексот. Ова овозможува пронаоѓање на возачот во O(1) односно константно време, без да се скенираат сите возачи.
create or replace procedure create_offer(
v_request_id int4,
v_dispatcher_user_id int4,
v_driver_user_id int4,
v_price numeric(19, 2),
v_currency_catalog_id int4,
v_eta timestamp
)
language plpgsql
AS
$$
declare
v_dispatcher_company_id int4;
v_start_position geometry;
v_customer_user_id int4;
v_computed_driver_id int4;
begin
if not exists(select * from request where request.id = v_request_id and request.status = 'pending') then
raise exception 'Request with id % and pending status does not exist', v_request_id;
end if;
if v_price <= 0 then
raise exception 'Price has to be greater than 0';
end if;
if v_eta <= now() then
raise exception 'ETA cannot be lower than the time of creation';
end if;
v_customer_user_id := (select customer_user_id from request where request.id = v_request_id);
if v_dispatcher_user_id is not null then
v_dispatcher_company_id := (select eh.company_id
from dispatcher d
join employmenthistory eh on eh.employee_user_id = d.user_id and
(eh.end_date is null or eh.end_date > now())
where d.user_id = v_dispatcher_user_id
limit 1);
if v_dispatcher_company_id is null then
raise exception 'Dispatcher % is not assigned to a company', v_dispatcher_user_id;
end if;
v_start_position := (select start_location from request where id = v_request_id);
v_computed_driver_id := (select d.user_id
from driver d
join driver_vehicle dc on d.user_id = dc.id_driver
join employmenthistory eh
on eh.employee_user_id = d.user_id and (eh.end_date is null or
eh.end_date > now())
where dc.time_to is null
and eh.company_id = v_dispatcher_company_id
and d.location is not null
order by d.location <-> v_start_position
limit 1);
if v_computed_driver_id is null then
raise exception 'No available drivers found for request %', v_request_id;
end if;
else
if v_driver_user_id is null then
raise exception 'A driver_user_id must be specified if dispatcher is null';
end if;
if not exists(select 1 from freelancedriver where driver_user_id = v_driver_user_id) then
raise exception 'Driver % is not a registered freelance driver', v_driver_user_id;
end if;
v_computed_driver_id := v_driver_user_id;
end if;
insert into offer(status, created_at, request_id, dispatcher_user_id, driver_user_id, price, currency_catalog_id,
eta, customer_user_id)
values ('pending', now(), v_request_id, v_dispatcher_user_id, v_computed_driver_id,
v_price, v_currency_catalog_id, v_eta, v_customer_user_id);
commit;
end;
$$;
Останати рефакторирани процедури
create or replace procedure make_request(
out request_id int4,
in customer_id int4,
in start_position geometry,
in end_position geometry,
in number_of_adult_passengers int4 default 1,
in number_of_children int4 default 0,
in female_driver boolean default false,
in luggage boolean default false,
in luggage_count int4 default 0,
in baby_seat_count int4 default 0
)
language plpgsql as
$$
begin
if not exists(select user_id
from customer c
where c.user_id = customer_id)
then
raise exception 'Customer with id % does not exist', customer_id;
end if;
if number_of_adult_passengers < 0 then
raise exception 'Number of adult passengers cannot be negative';
end if;
if number_of_children < 0 then
raise exception 'Number of children cannot be negative';
end if;
if number_of_adult_passengers + number_of_children < 0 then
raise exception 'There must be at least 1 passenger';
end if;
if baby_seat_count < 0 then
raise exception 'Baby seat count cannot be negative';
end if;
if luggage = false and luggage_count > 0 then
raise exception 'Luggage count cannot be greater than zero if the luggage flag is set to false.';
end if;
if luggage = true and luggage_count <= 0 then
raise exception 'Invalid luggage count parameters';
end if;
if start_position = end_position then
raise exception 'Start and destination location cannot be the same';
end if;
insert into request(customer_user_id, start_latitude, start_longitude, end_latitude, end_longitude, timestamp,
number_of_adult_passengers, number_of_children, status, female_driver, luggage, luggage_count,
baby_seat_count, start_location,
end_location)
values (customer_id,
ST_Y(start_position::geometry),
ST_X(start_position::geometry),
ST_Y(end_position::geometry),
ST_X(end_position::geometry),
now(),
make_request.number_of_adult_passengers,
make_request.number_of_children,
'pending',
make_request.female_driver,
make_request.luggage,
make_request.luggage_count,
make_request.baby_seat_count,
make_request.start_position,
make_request.end_position)
returning id into request_id;
commit;
end;
$$;
create or replace procedure start_ride(
in v_offer_id int4
)
language plpgsql as
$$
declare
v_request_id int4;
v_driver_id int4;
v_vehicle_vin varchar(17);
begin
if not exists(select id from offer where id = v_offer_id) then
raise exception 'Offer with id % does not exist', v_offer_id;
end if;
if not exists (select 1
from offer
where id = v_offer_id
and status = 'pending') then
raise exception 'Offer % is not pending', v_offer_id;
end if;
v_request_id := (select o.request_id
from offer o
where o.id = v_offer_id);
v_driver_id := (select driver_user_id from offer where id = v_offer_id);
if v_driver_id is null then
raise exception 'No active drivers available';
end if;
v_vehicle_vin := (select vin_vehicle
from driver_vehicle
where id_driver = v_driver_id
and (time_to is null or time_to > now())
limit 1);
if v_vehicle_vin is null then
raise exception 'No active vehicle for driver %', v_driver_id;
end if;
insert into ride(start_time,
end_time,
distance_traveled,
vehicle_vin,
driver_user_id,
request_id,
status,
offer_id)
values (now(),
null,
0,
v_vehicle_vin,
v_driver_id,
v_request_id,
'in_progress',
v_offer_id);
update offer
set status='accepted'
where id = v_offer_id;
update request
set status='accepted'
where id = v_request_id;
commit;
end;
$$;
create or replace procedure add_waypoint(
in v_request_id int4,
in v_location geometry
)
language plpgsql as
$$
declare
v_prev_seqno int4;
begin
if not exists(select id from request where id = v_request_id) then
raise exception 'Request with id % does not exist', v_request_id;
end if;
if exists(select id
from waypoints w
where w.request_id = v_request_id
and ST_DWithin(w.location, v_location, 0.000001)) then
raise exception 'Waypoint already added';
end if;
v_prev_seqno := (select count(*)
from waypoints w
where w.request_id = v_request_id);
insert into waypoints(latitude, longitude, seqno, request_id, location)
values (st_y(v_location), st_x(v_location), v_prev_seqno + 1, v_request_id, v_location);
commit;
end;
$$;
create or replace procedure write_report(
id_ride int,
user_id int,
new_message text,
new_title text,
incident_location geometry,
new_reason text
)
language plpgsql
as $$
begin
if not exists (
select 1
from Ride r
join Request req on r.request_id = req.id
where r.id = id_ride and req.customer_user_id = user_id
) then
raise exception 'Invalid report: Customer % was not part of Ride %', user_id, id_ride;
end if;
if exists (
select 1
from Report
where ride_id = id_ride and customer_user_id = user_id
) then
raise exception 'Duplicate report: Customer % has already reported Ride %', user_id, id_ride;
end if;
if trim(new_message) = '' or trim(new_title) = '' or trim(new_reason) = '' then
raise exception 'Validation error: Title, message, and reason cannot be empty strings.';
end if;
insert into report(ride_id, customer_user_id, message, title, location, latitude, longitude, reason)
values (
id_ride,
user_id,
new_message,
new_title,
st_setsrid(incident_location, 4326),
ST_Y(incident_location),
ST_X(incident_location),
new_reason
);
commit;
end;
$$;
Lambda Архитектура и 4D Траектории на возење
За следење на возилата во реално време, воведена е Lambda архитектура. Табелата location служи како "Hot storage" каде што се запишуваат илјадници GPS точки додека возилото се движи. Кога возењето ќе заврши, се активира тригер кој ги собира сите точки и ги компресира во една 4-димензионална линија (LineStringZM – каде Z е брзината, а M е Unix времето) во табелата ride ("Cold storage"). Откако траекторијата е зачувана, сировите точки се бришат за да се ослободи меморија.
alter table location
add column speed numeric(5,2) default 0;
alter table ride
add column route_path geometry(LineStringZM, 4326);
create or replace function archive_ride_trajectory()
returns trigger
language plpgsql
as $$
begin
if NEW.status = 'completed' and OLD.status != 'completed' then
NEW.route_path := (
select ST_MakeLine(
ST_MakePoint(
l.longitude,
l.latitude,
COALESCE(l.speed, 0),
extract(epoch from l.timestamp)
)
order by l.timestamp
)
from location l
where l.ride_id = NEW.id
);
delete from location where ride_id = NEW.id;
end if;
return NEW;
end;
$$;
create trigger trigger_archive_trajectory
before update on ride
for each row
execute function archive_ride_trajectory();
update ride r
set route_path = (
select ST_MakeLine(
ST_MakePoint(
l.longitude,
l.latitude,
COALESCE(l.speed, 0),
extract(epoch from l.timestamp)
)
order by l.timestamp
)
from location l
where l.ride_id = r.id
)
where r.status = 'completed' and r.route_path is null;
delete from location
where ride_id in (select id from ride where status = 'completed');
Attachments (1)
- 1.png (10.1 KB ) - added by 26 hours ago.
Download all attachments as: .zip
