Loading...
Loading...
Compare original and translation side by side
geography| Type | Use Case | Distance Unit | Earth Curvature |
|---|---|---|---|
| GPS coordinates, long distances | Meters | Accounts for curvature |
| Local/planar operations, contains checks | Projection units | Ignores curvature |
-- Geography: accurate distances in meters for GPS data
SELECT ST_Distance(
ST_MakePoint(-87.6298, 41.8781)::geography, -- Chicago
ST_MakePoint(-122.4194, 37.7749)::geography -- San Francisco
) / 1609.34 AS distance_miles;
-- Returns: ~1856 miles (accurate)
-- Geometry: faster but less accurate for large distances
SELECT ST_Distance(
ST_SetSRID(ST_MakePoint(-87.6298, 41.8781), 4326),
ST_SetSRID(ST_MakePoint(-122.4194, 37.7749), 4326)
) AS distance_degrees;
-- Returns: degrees (must convert, less accurate for long distances)geography| 类型 | 适用场景 | 距离单位 | 地球曲率处理 |
|---|---|---|---|
| GPS坐标、长距离计算 | Meters | 考虑地球曲率 |
| 本地/平面操作、包含性检查 | 投影单位 | 忽略地球曲率 |
-- Geography: 对GPS数据计算精确的米制距离
SELECT ST_Distance(
ST_MakePoint(-87.6298, 41.8781)::geography, -- Chicago
ST_MakePoint(-122.4194, 37.7749)::geography -- San Francisco
) / 1609.34 AS distance_miles;
-- Returns: ~1856 miles (accurate)
-- Geometry: 速度更快但长距离计算精度较低
SELECT ST_Distance(
ST_SetSRID(ST_MakePoint(-87.6298, 41.8781), 4326),
ST_SetSRID(ST_MakePoint(-122.4194, 37.7749), 4326)
) AS distance_degrees;
-- Returns: degrees (must convert, less accurate for long distances)-- Creating a point from GPS coordinates
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)
-- Creating a geography point (preferred for distance calculations)
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
-- Example: Create point for a facility location
ST_SetSRID(ST_MakePoint(-87.6298, 41.8781), 4326)::geography-- 从GPS坐标创建点
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)
-- 创建geography类型的点(推荐用于距离计算)
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
-- 示例:为设施位置创建点
ST_SetSRID(ST_MakePoint(-87.6298, 41.8781), 4326)::geography-- Migration: Create geofences table
CREATE TABLE public.geofences (
-- Primary key
id UUID DEFAULT gen_random_uuid() NOT NULL,
-- Identification
name TEXT NOT NULL,
description TEXT,
-- Relationship to facility (optional - standalone geofences allowed)
facility_id UUID REFERENCES facilities(id) ON DELETE SET NULL,
-- Spatial boundary - geography type for accurate distance calculations
boundary geography(Polygon, 4326) NOT NULL,
-- For circular geofences, store radius for reference
radius_miles NUMERIC(10,2),
-- Geofence type for business logic
geofence_type TEXT NOT NULL DEFAULT 'facility',
-- Active flag for enabling/disabling
is_active BOOLEAN NOT NULL DEFAULT true,
-- Standard audit columns
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
created_by INT4,
updated_by INT4,
deleted_at TIMESTAMPTZ,
deleted_by INT4,
CONSTRAINT geofences_pkey PRIMARY KEY (id),
CONSTRAINT chk_geofences_type CHECK (
geofence_type IN ('facility', 'city', 'region', 'custom')
)
);
COMMENT ON TABLE public.geofences IS
'Geographic boundaries for tracking events (arrival, departure, dwell time)';
COMMENT ON COLUMN public.geofences.boundary IS
'Polygon boundary in WGS84 (SRID 4326). Use geography type for accurate distance calculations';
COMMENT ON COLUMN public.geofences.radius_miles IS
'For circular geofences, the original radius used to generate the boundary polygon';-- 迁移脚本:创建geofences表
CREATE TABLE public.geofences (
-- 主键
id UUID DEFAULT gen_random_uuid() NOT NULL,
-- 标识信息
name TEXT NOT NULL,
description TEXT,
-- 与设施的关联关系(可选 - 允许独立地理围栏)
facility_id UUID REFERENCES facilities(id) ON DELETE SET NULL,
-- 空间边界 - 使用geography类型以实现精确的距离计算
boundary geography(Polygon, 4326) NOT NULL,
-- 对于圆形地理围栏,存储参考半径
radius_miles NUMERIC(10,2),
-- 用于业务逻辑的地理围栏类型
geofence_type TEXT NOT NULL DEFAULT 'facility',
-- 启用/禁用的状态标识
is_active BOOLEAN NOT NULL DEFAULT true,
-- 标准审计列
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
created_by INT4,
updated_by INT4,
deleted_at TIMESTAMPTZ,
deleted_by INT4,
CONSTRAINT geofences_pkey PRIMARY KEY (id),
CONSTRAINT chk_geofences_type CHECK (
geofence_type IN ('facility', 'city', 'region', 'custom')
)
);
COMMENT ON TABLE public.geofences IS
'用于追踪事件(到达、离开、停留时间)的地理边界';
COMMENT ON COLUMN public.geofences.boundary IS
'WGS84坐标系(SRID 4326)下的多边形边界。使用geography类型以实现精确的距离计算';
COMMENT ON COLUMN public.geofences.radius_miles IS
'对于圆形地理围栏,用于生成边界多边形的原始半径';-- Create a circular geofence around a facility
INSERT INTO geofences (name, facility_id, boundary, radius_miles, geofence_type)
SELECT
f.name || ' Geofence',
f.id,
-- ST_Buffer creates a circle; convert miles to meters (1 mile = 1609.34 meters)
ST_Buffer(
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography,
0.5 * 1609.34 -- 0.5 mile radius in meters
),
0.5,
'facility'
FROM facilities f
WHERE f.id = $1;-- 为设施创建圆形地理围栏
INSERT INTO geofences (name, facility_id, boundary, radius_miles, geofence_type)
SELECT
f.name || ' Geofence',
f.id,
-- ST_Buffer创建圆形;将英里转换为米(1英里=1609.34米)
ST_Buffer(
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography,
0.5 * 1609.34 -- 0.5英里半径(米制)
),
0.5,
'facility'
FROM facilities f
WHERE f.id = $1;-- Create a custom polygon geofence (e.g., terminal yard)
INSERT INTO geofences (name, boundary, geofence_type)
VALUES (
'Terminal Yard A',
ST_GeomFromText(
'POLYGON((-87.630 41.878, -87.628 41.878, -87.628 41.876, -87.630 41.876, -87.630 41.878))',
4326
)::geography,
'custom'
);-- 创建自定义多边形地理围栏(例如:码头堆场)
INSERT INTO geofences (name, boundary, geofence_type)
VALUES (
'Terminal Yard A',
ST_GeomFromText(
'POLYGON((-87.630 41.878, -87.628 41.878, -87.628 41.876, -87.630 41.876, -87.630 41.878))',
4326
)::geography,
'custom'
);-- Migration: Add spatial indexes to geofences table
-- GIST index on geofence boundaries (required for spatial queries)
CREATE INDEX idx_geofences_boundary
ON public.geofences USING GIST(boundary);
-- Standard indexes
CREATE INDEX idx_geofences_facility_id
ON public.geofences(facility_id)
WHERE facility_id IS NOT NULL;
CREATE INDEX idx_geofences_deleted_at
ON public.geofences(deleted_at)
WHERE deleted_at IS NULL;
CREATE INDEX idx_geofences_is_active
ON public.geofences(is_active)
WHERE is_active = true;-- 迁移脚本:为geofences表添加空间索引
-- 为地理围栏边界创建GIST索引(空间查询必需)
CREATE INDEX idx_geofences_boundary
ON public.geofences USING GIST(boundary);
-- 标准索引
CREATE INDEX idx_geofences_facility_id
ON public.geofences(facility_id)
WHERE facility_id IS NOT NULL;
CREATE INDEX idx_geofences_deleted_at
ON public.geofences(deleted_at)
WHERE deleted_at IS NULL;
CREATE INDEX idx_geofences_is_active
ON public.geofences(is_active)
WHERE is_active = true;-- Create index on load_cognition for location-based queries
-- Note: Creates expression index since location is stored as lat/lon columns
CREATE INDEX idx_load_cognition_location
ON public.load_cognition USING GIST(
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
);
-- Alternative: Partial index for valid coordinates only
CREATE INDEX idx_load_cognition_location_valid
ON public.load_cognition USING GIST(
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
)
WHERE latitude IS NOT NULL
AND longitude IS NOT NULL
AND latitude BETWEEN -90 AND 90
AND longitude BETWEEN -180 AND 180;-- 为load_cognition表创建基于位置查询的索引
-- 注意:由于位置存储为lat/lon列,因此创建表达式索引
CREATE INDEX idx_load_cognition_location
ON public.load_cognition USING GIST(
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
);
-- 替代方案:仅为有效坐标创建部分索引
CREATE INDEX idx_load_cognition_location_valid
ON public.load_cognition USING GIST(
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
)
WHERE latitude IS NOT NULL
AND longitude IS NOT NULL
AND latitude BETWEEN -90 AND 90
AND longitude BETWEEN -180 AND 180;-- Create spatial index on facilities for proximity queries
CREATE INDEX idx_facilities_location
ON public.facilities USING GIST(
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
)
WHERE latitude IS NOT NULL AND longitude IS NOT NULL;-- 为facilities表创建空间索引以支持邻近查询
CREATE INDEX idx_facilities_location
ON public.facilities USING GIST(
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
)
WHERE latitude IS NOT NULL AND longitude IS NOT NULL;-- Distance from truck to destination facility (in miles)
SELECT
lc.load_id,
ST_Distance(
ST_SetSRID(ST_MakePoint(lc.longitude, lc.latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography
) / 1609.34 AS distance_miles
FROM load_cognition lc
JOIN loads l ON lc.load_id = l.id
JOIN stops s ON l.id = s.load_id AND s.stop_type = 'destination'
JOIN facilities f ON s.facility_id = f.id
WHERE lc.load_id = $1
ORDER BY lc.cognition_time DESC
LIMIT 1;-- 计算卡车到目的地设施的距离(英里)
SELECT
lc.load_id,
ST_Distance(
ST_SetSRID(ST_MakePoint(lc.longitude, lc.latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography
) / 1609.34 AS distance_miles
FROM load_cognition lc
JOIN loads l ON lc.load_id = l.id
JOIN stops s ON l.id = s.load_id AND s.stop_type = 'destination'
JOIN facilities f ON s.facility_id = f.id
WHERE lc.load_id = $1
ORDER BY lc.cognition_time DESC
LIMIT 1;-- Find all trucks within 50 miles of a facility
SELECT
lc.load_id,
l.load_number,
lc.driver_name,
ST_Distance(
ST_SetSRID(ST_MakePoint(lc.longitude, lc.latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography
) / 1609.34 AS distance_miles
FROM load_cognition lc
JOIN loads l ON lc.load_id = l.id
CROSS JOIN (
SELECT longitude, latitude FROM facilities WHERE id = $1
) f
WHERE ST_DWithin(
ST_SetSRID(ST_MakePoint(lc.longitude, lc.latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography,
50 * 1609.34 -- 50 miles in meters
)
AND lc.cognition_time > now() - INTERVAL '1 hour'
ORDER BY distance_miles;-- 查找设施50英里范围内的所有卡车
SELECT
lc.load_id,
l.load_number,
lc.driver_name,
ST_Distance(
ST_SetSRID(ST_MakePoint(lc.longitude, lc.latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography
) / 1609.34 AS distance_miles
FROM load_cognition lc
JOIN loads l ON lc.load_id = l.id
CROSS JOIN (
SELECT longitude, latitude FROM facilities WHERE id = $1
) f
WHERE ST_DWithin(
ST_SetSRID(ST_MakePoint(lc.longitude, lc.latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography,
50 * 1609.34 -- 50英里(米制)
)
AND lc.cognition_time > now() - INTERVAL '1 hour'
ORDER BY distance_miles;-- Check if truck is inside any active geofence
SELECT g.id, g.name, g.facility_id
FROM geofences g
WHERE ST_Contains(
g.boundary::geometry,
ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)
)
AND g.is_active = true
AND g.deleted_at IS NULL;-- 检查卡车是否处于任何活跃的地理围栏内
SELECT g.id, g.name, g.facility_id
FROM geofences g
WHERE ST_Contains(
g.boundary::geometry,
ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)
)
AND g.is_active = true
AND g.deleted_at IS NULL;-- Create a 10-mile buffer zone around current truck position
SELECT ST_Buffer(
ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)::geography,
10 * 1609.34 -- 10 miles in meters
) AS buffer_zone;
-- Find facilities within buffer
SELECT f.id, f.name, f.city, f.state
FROM facilities f
WHERE ST_DWithin(
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)::geography,
10 * 1609.34
)
AND f.deleted_at IS NULL;-- 为当前卡车位置创建10英里的缓冲区
SELECT ST_Buffer(
ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)::geography,
10 * 1609.34 -- 10英里(米制)
) AS buffer_zone;
-- 查找缓冲区内的设施
SELECT f.id, f.name, f.city, f.state
FROM facilities f
WHERE ST_DWithin(
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)::geography,
10 * 1609.34
)
AND f.deleted_at IS NULL;-- Function to check if a position is inside any geofence
CREATE OR REPLACE FUNCTION public.check_geofence_entry(
p_longitude NUMERIC,
p_latitude NUMERIC
)
RETURNS TABLE (
geofence_id UUID,
geofence_name TEXT,
facility_id UUID,
geofence_type TEXT
)
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = 'public'
AS $$
BEGIN
RETURN QUERY
SELECT
g.id,
g.name,
g.facility_id,
g.geofence_type
FROM geofences g
WHERE ST_Contains(
g.boundary::geometry,
ST_SetSRID(ST_MakePoint(p_longitude, p_latitude), 4326)
)
AND g.is_active = true
AND g.deleted_at IS NULL;
END;
$$;
COMMENT ON FUNCTION public.check_geofence_entry(NUMERIC, NUMERIC) IS
'Returns all active geofences containing the given GPS coordinates';-- 检查位置是否处于任何地理围栏内的函数
CREATE OR REPLACE FUNCTION public.check_geofence_entry(
p_longitude NUMERIC,
p_latitude NUMERIC
)
RETURNS TABLE (
geofence_id UUID,
geofence_name TEXT,
facility_id UUID,
geofence_type TEXT
)
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = 'public'
AS $$
BEGIN
RETURN QUERY
SELECT
g.id,
g.name,
g.facility_id,
g.geofence_type
FROM geofences g
WHERE ST_Contains(
g.boundary::geometry,
ST_SetSRID(ST_MakePoint(p_longitude, p_latitude), 4326)
)
AND g.is_active = true
AND g.deleted_at IS NULL;
END;
$$;
COMMENT ON FUNCTION public.check_geofence_entry(NUMERIC, NUMERIC) IS
'返回包含给定GPS坐标的所有活跃地理围栏';-- Table to track geofence entry/exit events
CREATE TABLE public.geofence_events (
id UUID DEFAULT gen_random_uuid() NOT NULL,
load_id UUID NOT NULL REFERENCES loads(id) ON DELETE CASCADE,
geofence_id UUID NOT NULL REFERENCES geofences(id) ON DELETE CASCADE,
event_type TEXT NOT NULL, -- 'entry', 'exit', 'dwell'
event_time TIMESTAMPTZ NOT NULL DEFAULT now(),
-- Position at time of event
latitude NUMERIC(10,7),
longitude NUMERIC(11,7),
-- Dwell time tracking (for exit events)
entry_time TIMESTAMPTZ,
dwell_minutes INTEGER,
-- Standard audit columns
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT geofence_events_pkey PRIMARY KEY (id),
CONSTRAINT chk_geofence_events_type CHECK (
event_type IN ('entry', 'exit', 'dwell')
)
);
CREATE INDEX idx_geofence_events_load_id ON geofence_events(load_id);
CREATE INDEX idx_geofence_events_geofence_id ON geofence_events(geofence_id);
CREATE INDEX idx_geofence_events_event_time ON geofence_events(event_time);-- 用于追踪地理围栏进入/退出事件的表
CREATE TABLE public.geofence_events (
id UUID DEFAULT gen_random_uuid() NOT NULL,
load_id UUID NOT NULL REFERENCES loads(id) ON DELETE CASCADE,
geofence_id UUID NOT NULL REFERENCES geofences(id) ON DELETE CASCADE,
event_type TEXT NOT NULL, -- 'entry', 'exit', 'dwell'
event_time TIMESTAMPTZ NOT NULL DEFAULT now(),
-- 事件发生时的位置
latitude NUMERIC(10,7),
longitude NUMERIC(11,7),
-- 停留时间追踪(针对退出事件)
entry_time TIMESTAMPTZ,
dwell_minutes INTEGER,
-- 标准审计列
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT geofence_events_pkey PRIMARY KEY (id),
CONSTRAINT chk_geofence_events_type CHECK (
event_type IN ('entry', 'exit', 'dwell')
)
);
CREATE INDEX idx_geofence_events_load_id ON geofence_events(load_id);
CREATE INDEX idx_geofence_events_geofence_id ON geofence_events(geofence_id);
CREATE INDEX idx_geofence_events_event_time ON geofence_events(event_time);-- Function to process location update and detect geofence events
CREATE OR REPLACE FUNCTION public.process_location_geofence(
p_load_id UUID,
p_longitude NUMERIC,
p_latitude NUMERIC,
p_timestamp TIMESTAMPTZ DEFAULT now()
)
RETURNS SETOF geofence_events
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = 'public'
AS $$
DECLARE
v_current_geofences UUID[];
v_previous_geofences UUID[];
v_entered_geofence UUID;
v_exited_geofence UUID;
v_entry_time TIMESTAMPTZ;
v_event geofence_events;
BEGIN
-- Get current geofences containing this position
SELECT ARRAY_AGG(g.id) INTO v_current_geofences
FROM geofences g
WHERE ST_Contains(
g.boundary::geometry,
ST_SetSRID(ST_MakePoint(p_longitude, p_latitude), 4326)
)
AND g.is_active = true
AND g.deleted_at IS NULL;
-- Get previously active geofences for this load
SELECT ARRAY_AGG(DISTINCT ge.geofence_id) INTO v_previous_geofences
FROM geofence_events ge
WHERE ge.load_id = p_load_id
AND ge.event_type = 'entry'
AND NOT EXISTS (
SELECT 1 FROM geofence_events ge2
WHERE ge2.load_id = p_load_id
AND ge2.geofence_id = ge.geofence_id
AND ge2.event_type = 'exit'
AND ge2.event_time > ge.event_time
);
-- Handle NULL arrays
v_current_geofences := COALESCE(v_current_geofences, ARRAY[]::UUID[]);
v_previous_geofences := COALESCE(v_previous_geofences, ARRAY[]::UUID[]);
-- Detect entries (in current but not in previous)
FOR v_entered_geofence IN
SELECT UNNEST(v_current_geofences)
EXCEPT
SELECT UNNEST(v_previous_geofences)
LOOP
INSERT INTO geofence_events (load_id, geofence_id, event_type, event_time, latitude, longitude)
VALUES (p_load_id, v_entered_geofence, 'entry', p_timestamp, p_latitude, p_longitude)
RETURNING * INTO v_event;
RETURN NEXT v_event;
END LOOP;
-- Detect exits (in previous but not in current)
FOR v_exited_geofence IN
SELECT UNNEST(v_previous_geofences)
EXCEPT
SELECT UNNEST(v_current_geofences)
LOOP
-- Get entry time for dwell calculation
SELECT ge.event_time INTO v_entry_time
FROM geofence_events ge
WHERE ge.load_id = p_load_id
AND ge.geofence_id = v_exited_geofence
AND ge.event_type = 'entry'
ORDER BY ge.event_time DESC
LIMIT 1;
INSERT INTO geofence_events (
load_id, geofence_id, event_type, event_time,
latitude, longitude, entry_time, dwell_minutes
)
VALUES (
p_load_id, v_exited_geofence, 'exit', p_timestamp,
p_latitude, p_longitude, v_entry_time,
EXTRACT(EPOCH FROM (p_timestamp - v_entry_time)) / 60
)
RETURNING * INTO v_event;
RETURN NEXT v_event;
END LOOP;
RETURN;
END;
$$;
COMMENT ON FUNCTION public.process_location_geofence(UUID, NUMERIC, NUMERIC, TIMESTAMPTZ) IS
'Processes location update and generates geofence entry/exit events';-- 处理位置更新并检测地理围栏事件的函数
CREATE OR REPLACE FUNCTION public.process_location_geofence(
p_load_id UUID,
p_longitude NUMERIC,
p_latitude NUMERIC,
p_timestamp TIMESTAMPTZ DEFAULT now()
)
RETURNS SETOF geofence_events
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = 'public'
AS $$
DECLARE
v_current_geofences UUID[];
v_previous_geofences UUID[];
v_entered_geofence UUID;
v_exited_geofence UUID;
v_entry_time TIMESTAMPTZ;
v_event geofence_events;
BEGIN
-- 获取当前位置所属的地理围栏
SELECT ARRAY_AGG(g.id) INTO v_current_geofences
FROM geofences g
WHERE ST_Contains(
g.boundary::geometry,
ST_SetSRID(ST_MakePoint(p_longitude, p_latitude), 4326)
)
AND g.is_active = true
AND g.deleted_at IS NULL;
-- 获取该货物之前活跃的地理围栏
SELECT ARRAY_AGG(DISTINCT ge.geofence_id) INTO v_previous_geofences
FROM geofence_events ge
WHERE ge.load_id = p_load_id
AND ge.event_type = 'entry'
AND NOT EXISTS (
SELECT 1 FROM geofence_events ge2
WHERE ge2.load_id = p_load_id
AND ge2.geofence_id = ge.geofence_id
AND ge2.event_type = 'exit'
AND ge2.event_time > ge.event_time
);
-- 处理NULL数组
v_current_geofences := COALESCE(v_current_geofences, ARRAY[]::UUID[]);
v_previous_geofences := COALESCE(v_previous_geofences, ARRAY[]::UUID[]);
-- 检测进入事件(当前在围栏内但之前不在)
FOR v_entered_geofence IN
SELECT UNNEST(v_current_geofences)
EXCEPT
SELECT UNNEST(v_previous_geofences)
LOOP
INSERT INTO geofence_events (load_id, geofence_id, event_type, event_time, latitude, longitude)
VALUES (p_load_id, v_entered_geofence, 'entry', p_timestamp, p_latitude, p_longitude)
RETURNING * INTO v_event;
RETURN NEXT v_event;
END LOOP;
-- 检测退出事件(之前在围栏内但当前不在)
FOR v_exited_geofence IN
SELECT UNNEST(v_previous_geofences)
EXCEPT
SELECT UNNEST(v_current_geofences)
LOOP
-- 获取进入时间以计算停留时间
SELECT ge.event_time INTO v_entry_time
FROM geofence_events ge
WHERE ge.load_id = p_load_id
AND ge.geofence_id = v_exited_geofence
AND ge.event_type = 'entry'
ORDER BY ge.event_time DESC
LIMIT 1;
INSERT INTO geofence_events (
load_id, geofence_id, event_type, event_time,
latitude, longitude, entry_time, dwell_minutes
)
VALUES (
p_load_id, v_exited_geofence, 'exit', p_timestamp,
p_latitude, p_longitude, v_entry_time,
EXTRACT(EPOCH FROM (p_timestamp - v_entry_time)) / 60
)
RETURNING * INTO v_event;
RETURN NEXT v_event;
END LOOP;
RETURN;
END;
$$;
COMMENT ON FUNCTION public.process_location_geofence(UUID, NUMERIC, NUMERIC, TIMESTAMPTZ) IS
'处理位置更新并生成地理围栏进入/退出事件';-- Calculate simple ETA based on distance and average speed
CREATE OR REPLACE FUNCTION public.calculate_eta(
p_current_lon NUMERIC,
p_current_lat NUMERIC,
p_dest_lon NUMERIC,
p_dest_lat NUMERIC,
p_avg_speed_mph NUMERIC DEFAULT 50
)
RETURNS TIMESTAMPTZ
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = 'public'
AS $$
DECLARE
v_distance_miles NUMERIC;
v_hours NUMERIC;
BEGIN
-- Calculate distance in miles
v_distance_miles := ST_Distance(
ST_SetSRID(ST_MakePoint(p_current_lon, p_current_lat), 4326)::geography,
ST_SetSRID(ST_MakePoint(p_dest_lon, p_dest_lat), 4326)::geography
) / 1609.34;
-- Calculate travel time
v_hours := v_distance_miles / p_avg_speed_mph;
RETURN now() + (v_hours || ' hours')::INTERVAL;
END;
$$;
COMMENT ON FUNCTION public.calculate_eta(NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC) IS
'Calculates ETA based on straight-line distance and average speed. For accurate routing, integrate with external routing API';-- 根据距离和平均速度计算简单ETA
CREATE OR REPLACE FUNCTION public.calculate_eta(
p_current_lon NUMERIC,
p_current_lat NUMERIC,
p_dest_lon NUMERIC,
p_dest_lat NUMERIC,
p_avg_speed_mph NUMERIC DEFAULT 50
)
RETURNS TIMESTAMPTZ
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = 'public'
AS $$
DECLARE
v_distance_miles NUMERIC;
v_hours NUMERIC;
BEGIN
-- 计算英里距离
v_distance_miles := ST_Distance(
ST_SetSRID(ST_MakePoint(p_current_lon, p_current_lat), 4326)::geography,
ST_SetSRID(ST_MakePoint(p_dest_lon, p_dest_lat), 4326)::geography
) / 1609.34;
-- 计算行驶时间
v_hours := v_distance_miles / p_avg_speed_mph;
RETURN now() + (v_hours || ' hours')::INTERVAL;
END;
$$;
COMMENT ON FUNCTION public.calculate_eta(NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC) IS
'基于直线距离和平均速度计算ETA。若需精确路由,请集成外部路由API';-- Calculate ETA to next stop for a load
SELECT
l.load_number,
s.stop_sequence,
f.name AS facility_name,
ST_Distance(
ST_SetSRID(ST_MakePoint(lc.longitude, lc.latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography
) / 1609.34 AS distance_miles,
now() + (
(ST_Distance(
ST_SetSRID(ST_MakePoint(lc.longitude, lc.latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography
) / 1609.34) / 50 -- Assume 50 mph average
|| ' hours'
)::INTERVAL AS estimated_arrival
FROM loads l
JOIN stops s ON l.id = s.load_id
JOIN facilities f ON s.facility_id = f.id
JOIN LATERAL (
SELECT longitude, latitude
FROM load_cognition
WHERE load_id = l.id
ORDER BY cognition_time DESC
LIMIT 1
) lc ON true
WHERE l.id = $1
AND s.actual_arrival IS NULL -- Not yet arrived
ORDER BY s.stop_sequence
LIMIT 1;-- 计算货物到下一个停靠点的ETA
SELECT
l.load_number,
s.stop_sequence,
f.name AS facility_name,
ST_Distance(
ST_SetSRID(ST_MakePoint(lc.longitude, lc.latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography
) / 1609.34 AS distance_miles,
now() + (
(ST_Distance(
ST_SetSRID(ST_MakePoint(lc.longitude, lc.latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326)::geography
) / 1609.34) / 50 -- 假设平均速度50英里/小时
|| ' hours'
)::INTERVAL AS estimated_arrival
FROM loads l
JOIN stops s ON l.id = s.load_id
JOIN facilities f ON s.facility_id = f.id
JOIN LATERAL (
SELECT longitude, latitude
FROM load_cognition
WHERE load_id = l.id
ORDER BY cognition_time DESC
LIMIT 1
) lc ON true
WHERE l.id = $1
AND s.actual_arrival IS NULL -- 尚未到达
ORDER BY s.stop_sequence
LIMIT 1;-- PGRouting requires a road network table
-- This is typically populated from OpenStreetMap data
-- Example: Calculate route distance using Dijkstra algorithm
-- Note: Requires pgr_createTopology and road network data
-- SELECT
-- sum(cost) AS total_distance,
-- ST_Union(geom) AS route_geometry
-- FROM pgr_dijkstra(
-- 'SELECT id, source, target, cost FROM roads',
-- $start_node, $end_node,
-- directed := false
-- ) AS route
-- JOIN roads ON route.edge = roads.id;
-- For production ETA calculations, consider:
-- 1. External routing APIs (OSRM, Google Directions, Here)
-- 2. Pre-calculated route distances in lanes table
-- 3. Mileage tables from PC*MILER or similar-- PGRouting需要道路网络表
-- 通常从OpenStreetMap数据中填充
-- 示例:使用Dijkstra算法计算路由距离
-- 注意:需要pgr_createTopology和道路网络数据
-- SELECT
-- sum(cost) AS total_distance,
-- ST_Union(geom) AS route_geometry
-- FROM pgr_dijkstra(
-- 'SELECT id, source, target, cost FROM roads',
-- $start_node, $end_node,
-- directed := false
-- ) AS route
-- JOIN roads ON route.edge = roads.id;
-- 生产环境ETA计算建议:
-- 1. 外部路由API(OSRM、Google Directions、Here)
-- 2. 在lanes表中预计算路由距离
-- 3. 使用PC*MILER等里程表load_cognition-- Key columns in load_cognition
-- id UUID
-- load_id UUID (FK to loads)
-- driver_name TEXT
-- latitude NUMERIC(10,7)
-- longitude NUMERIC(11,7)
-- cognition_time TIMESTAMPTZ
-- speed_mph NUMERIC
-- heading NUMERIC
-- source TEXT (e.g., 'eld', 'mobile', 'manual')load_cognition-- load_cognition表的关键列
-- id UUID
-- load_id UUID(关联loads表的外键)
-- driver_name TEXT
-- latitude NUMERIC(10,7)
-- longitude NUMERIC(11,7)
-- cognition_time TIMESTAMPTZ
-- speed_mph NUMERIC
-- heading NUMERIC
-- source TEXT(例如:'eld', 'mobile', 'manual')-- Get current position of all active loads
SELECT DISTINCT ON (l.id)
l.id AS load_id,
l.load_number,
l.load_status,
lc.driver_name,
lc.latitude,
lc.longitude,
lc.speed_mph,
lc.cognition_time,
-- Calculate time since last update
EXTRACT(EPOCH FROM (now() - lc.cognition_time)) / 60 AS minutes_since_update
FROM loads l
JOIN load_cognition lc ON l.id = lc.load_id
WHERE l.load_status IN ('dispatched', 'at_origin', 'in_transit', 'at_destination')
AND l.deleted_at IS NULL
AND lc.latitude IS NOT NULL
AND lc.longitude IS NOT NULL
ORDER BY l.id, lc.cognition_time DESC;-- 获取所有活跃货物的当前位置
SELECT DISTINCT ON (l.id)
l.id AS load_id,
l.load_number,
l.load_status,
lc.driver_name,
lc.latitude,
lc.longitude,
lc.speed_mph,
lc.cognition_time,
-- 计算上次更新至今的时间
EXTRACT(EPOCH FROM (now() - lc.cognition_time)) / 60 AS minutes_since_update
FROM loads l
JOIN load_cognition lc ON l.id = lc.load_id
WHERE l.load_status IN ('dispatched', 'at_origin', 'in_transit', 'at_destination')
AND l.deleted_at IS NULL
AND lc.latitude IS NOT NULL
AND lc.longitude IS NOT NULL
ORDER BY l.id, lc.cognition_time DESC;-- Get tracking history for a load (for route visualization)
SELECT
lc.latitude,
lc.longitude,
lc.cognition_time,
lc.speed_mph,
lc.heading
FROM load_cognition lc
WHERE lc.load_id = $1
AND lc.latitude IS NOT NULL
AND lc.longitude IS NOT NULL
ORDER BY lc.cognition_time ASC;-- 获取货物的历史轨迹(用于路线可视化)
SELECT
lc.latitude,
lc.longitude,
lc.cognition_time,
lc.speed_mph,
lc.heading
FROM load_cognition lc
WHERE lc.load_id = $1
AND lc.latitude IS NOT NULL
AND lc.longitude IS NOT NULL
ORDER BY lc.cognition_time ASC;-- Get tracking history as GeoJSON for map display
SELECT json_build_object(
'type', 'Feature',
'properties', json_build_object(
'load_id', $1::text,
'point_count', count(*)
),
'geometry', json_build_object(
'type', 'LineString',
'coordinates', json_agg(
json_build_array(longitude, latitude)
ORDER BY cognition_time
)
)
) AS geojson
FROM load_cognition
WHERE load_id = $1
AND latitude IS NOT NULL
AND longitude IS NOT NULL;-- 将历史轨迹转换为GeoJSON以用于地图展示
SELECT json_build_object(
'type', 'Feature',
'properties', json_build_object(
'load_id', $1::text,
'point_count', count(*)
),
'geometry', json_build_object(
'type', 'LineString',
'coordinates', json_agg(
json_build_array(longitude, latitude)
ORDER BY cognition_time
)
)
) AS geojson
FROM load_cognition
WHERE load_id = $1
AND latitude IS NOT NULL
AND longitude IS NOT NULL;-- Facilities as GeoJSON FeatureCollection
SELECT json_build_object(
'type', 'FeatureCollection',
'features', json_agg(
json_build_object(
'type', 'Feature',
'id', f.id,
'properties', json_build_object(
'name', f.name,
'city', f.city,
'state', f.state,
'facility_type', f.facility_type
),
'geometry', json_build_object(
'type', 'Point',
'coordinates', json_build_array(f.longitude, f.latitude)
)
)
)
) AS geojson
FROM facilities f
WHERE f.latitude IS NOT NULL
AND f.longitude IS NOT NULL
AND f.deleted_at IS NULL;-- 将设施转换为GeoJSON FeatureCollection
SELECT json_build_object(
'type', 'FeatureCollection',
'features', json_agg(
json_build_object(
'type', 'Feature',
'id', f.id,
'properties', json_build_object(
'name', f.name,
'city', f.city,
'state', f.state,
'facility_type', f.facility_type
),
'geometry', json_build_object(
'type', 'Point',
'coordinates', json_build_array(f.longitude, f.latitude)
)
)
)
) AS geojson
FROM facilities f
WHERE f.latitude IS NOT NULL
AND f.longitude IS NOT NULL
AND f.deleted_at IS NULL;-- Export geofences as GeoJSON for map overlay
SELECT json_build_object(
'type', 'FeatureCollection',
'features', json_agg(
json_build_object(
'type', 'Feature',
'id', g.id,
'properties', json_build_object(
'name', g.name,
'geofence_type', g.geofence_type,
'facility_id', g.facility_id
),
'geometry', ST_AsGeoJSON(g.boundary::geometry)::json
)
)
) AS geojson
FROM geofences g
WHERE g.is_active = true
AND g.deleted_at IS NULL;-- 将地理围栏导出为GeoJSON以用于地图叠加层
SELECT json_build_object(
'type', 'FeatureCollection',
'features', json_agg(
json_build_object(
'type', 'Feature',
'id', g.id,
'properties', json_build_object(
'name', g.name,
'geofence_type', g.geofence_type,
'facility_id', g.facility_id
),
'geometry', ST_AsGeoJSON(g.boundary::geometry)::json
)
)
) AS geojson
FROM geofences g
WHERE g.is_active = true
AND g.deleted_at IS NULL;-- Current fleet positions for real-time map
SELECT json_build_object(
'type', 'FeatureCollection',
'features', (
SELECT json_agg(
json_build_object(
'type', 'Feature',
'id', t.load_id,
'properties', json_build_object(
'load_number', t.load_number,
'load_status', t.load_status,
'driver_name', t.driver_name,
'speed_mph', t.speed_mph,
'last_update', t.cognition_time
),
'geometry', json_build_object(
'type', 'Point',
'coordinates', json_build_array(t.longitude, t.latitude)
)
)
)
FROM (
SELECT DISTINCT ON (l.id)
l.id AS load_id,
l.load_number,
l.load_status,
lc.driver_name,
lc.latitude,
lc.longitude,
lc.speed_mph,
lc.cognition_time
FROM loads l
JOIN load_cognition lc ON l.id = lc.load_id
WHERE l.load_status IN ('dispatched', 'at_origin', 'in_transit', 'at_destination')
AND l.deleted_at IS NULL
AND lc.latitude IS NOT NULL
ORDER BY l.id, lc.cognition_time DESC
) t
)
) AS geojson;-- 用于实时地图的当前车队位置
SELECT json_build_object(
'type', 'FeatureCollection',
'features', (
SELECT json_agg(
json_build_object(
'type', 'Feature',
'id', t.load_id,
'properties', json_build_object(
'load_number', t.load_number,
'load_status', t.load_status,
'driver_name', t.driver_name,
'speed_mph', t.speed_mph,
'last_update', t.cognition_time
),
'geometry', json_build_object(
'type', 'Point',
'coordinates', json_build_array(t.longitude, t.latitude)
)
)
)
FROM (
SELECT DISTINCT ON (l.id)
l.id AS load_id,
l.load_number,
l.load_status,
lc.driver_name,
lc.latitude,
lc.longitude,
lc.speed_mph,
lc.cognition_time
FROM loads l
JOIN load_cognition lc ON l.id = lc.load_id
WHERE l.load_status IN ('dispatched', 'at_origin', 'in_transit', 'at_destination')
AND l.deleted_at IS NULL
AND lc.latitude IS NOT NULL
ORDER BY l.id, lc.cognition_time DESC
) t
)
) AS geojson;layerchartlayerchartlayerchartlayerchart| Function | Purpose | Example |
|---|---|---|
| Create point from coordinates | |
| Assign coordinate system | |
| Distance between geometries (meters for geography) | |
| True if within distance | |
| True if polygon contains point | |
| Create buffer zone | |
| Export as GeoJSON | |
| Import from GeoJSON | |
| Merge geometries | |
| Center point of geometry | |
| 函数 | 用途 | 示例 |
|---|---|---|
| 从坐标创建点 | |
| 分配坐标系 | |
| 计算几何体间距离(geography类型返回米制) | |
| 判断是否在指定距离内 | |
| 判断多边形是否包含点 | |
| 创建缓冲区 | |
| 导出为GeoJSON | |
| 从GeoJSON导入 | |
| 合并几何体 | |
| 获取几何体中心点 | |
| From | To | Multiply By |
|---|---|---|
| Miles | Meters | 1609.34 |
| Meters | Miles | 0.000621371 |
| Kilometers | Miles | 0.621371 |
| Miles | Kilometers | 1.60934 |
| 从 | 到 | 转换系数 |
|---|---|---|
| 英里 | 米 | 1609.34 |
| 米 | 英里 | 0.000621371 |
| 千米 | 英里 | 0.621371 |
| 英里 | 千米 | 1.60934 |
PostGIS Setup:
[ ] PostGIS extension enabled (CREATE EXTENSION postgis)
[ ] PGRouting extension enabled if routing needed (CREATE EXTENSION pgrouting)
[ ] SRID 4326 used for all GPS coordinate data
[ ] Geography type used for distance calculations
Geofences:
[ ] Geofences table with geography(Polygon, 4326) boundary column
[ ] GIST index on boundary column
[ ] Functions for geofence entry/exit detection
[ ] Event table for tracking geofence transitions
Spatial Indexes:
[ ] GIST index on all geography/geometry columns
[ ] Expression index on load_cognition for location queries
[ ] Partial indexes for valid coordinate rows only
Tracking:
[ ] load_cognition captures lat/lon with each update
[ ] Efficient queries for current fleet position
[ ] Historical track queries return ordered points
[ ] GeoJSON output for frontend consumption
Frontend Integration:
[ ] GeoJSON endpoints for facilities, geofences, fleet positions
[ ] LayerChart components for geographic visualizations
[ ] Real-time position updates via Supabase realtime subscriptionsPostGIS 配置:
[ ] 已启用PostGIS扩展(CREATE EXTENSION postgis)
[ ] 若需要路由功能,启用PGRouting扩展(CREATE EXTENSION pgrouting)
[ ] 所有GPS坐标数据使用SRID 4326
[ ] 距离计算使用Geography类型
地理围栏:
[ ] 包含geography(Polygon, 4326)类型boundary列的geofences表
[ ] 为boundary列创建GIST索引
[ ] 实现地理围栏进入/退出检测的函数
[ ] 用于追踪地理围栏转换事件的事件表
空间索引:
[ ] 为所有geography/geometry列创建GIST索引
[ ] 为load_cognition表创建基于位置查询的表达式索引
[ ] 仅为有效坐标行创建部分索引
追踪功能:
[ ] load_cognition表在每次更新时捕获经纬度
[ ] 高效查询当前车队位置
[ ] 历史轨迹查询返回有序的点位
[ ] 提供供前端使用的GeoJSON输出
前端集成:
[ ] 提供设施、地理围栏、车队位置的GeoJSON接口
[ ] 使用LayerChart组件实现地理可视化
[ ] 通过Supabase实时订阅实现位置实时更新supabase:laneweaver-database-designlayerchartload-lifecycle-patternsgeographysupabase:laneweaver-database-designlayerchartload-lifecycle-patternsgeography