GIS Maritime Shipping: PostGIS-Compatible Geospatial Functions

Exercises all 18 PostGIS-compatible st_* functions against 5 cargo vessels navigating 10 world ports with 40 GPS position reports, distances, bearings, WKT harbor polygons, and point-in-polygon detection.

Category: spatial

Syntax

-- ============================================================================
-- SETUP, zone, schema, 3 Delta tables
-- ============================================================================
CREATE ZONE IF NOT EXISTS {{zone_name}} TYPE EXTERNAL
    COMMENT 'External and Delta tables, demo datasets';

CREATE SCHEMA IF NOT EXISTS {{zone_name}}.maritime
    COMMENT 'Maritime shipping, ports, vessels, and GPS positions';

CREATE DELTA TABLE IF NOT EXISTS {{zone_name}}.maritime.ports (
    port_id INT, port_name VARCHAR, country VARCHAR,
    lat DOUBLE, lng DOUBLE, harbor_wkt VARCHAR
) LOCATION '{{data_path}}/maritime_ports';

CREATE DELTA TABLE IF NOT EXISTS {{zone_name}}.maritime.vessels (
    vessel_id INT, vessel_name VARCHAR, vessel_type VARCHAR,
    flag_country VARCHAR, deadweight_tons INT
) LOCATION '{{data_path}}/maritime_vessels';

CREATE DELTA TABLE IF NOT EXISTS {{zone_name}}.maritime.positions (
    position_id INT, vessel_id INT, lat DOUBLE, lng DOUBLE,
    speed_knots DOUBLE, recorded_at VARCHAR
) LOCATION '{{data_path}}/maritime_positions';

-- ============================================================================
-- QUERIES, one per st_* family
-- ============================================================================
-- Great-circle distances between port pairs (km, haversine)
SELECT p1.port_name || ' -> ' || p2.port_name AS route,
       ROUND(st_distance(p1.lat, p1.lng, p2.lat, p2.lng) / 1000.0, 0) AS distance_km
FROM {{zone_name}}.maritime.ports p1
CROSS JOIN {{zone_name}}.maritime.ports p2
WHERE (p1.port_id, p2.port_id) IN ((1,3),(2,10),(5,1),(7,9),(8,4),(1,4))
ORDER BY distance_km;

-- Algorithm comparison, haversine vs sphere vs Vincenty (Singapore -> Rotterdam)
SELECT ROUND(st_distance_haversine(p1.lat, p1.lng, p2.lat, p2.lng) / 1000.0, 0) AS haversine_km,
       ROUND(st_distance_sphere  (p1.lat, p1.lng, p2.lat, p2.lng) / 1000.0, 0) AS sphere_km,
       ROUND(st_distance_vincenty(p1.lat, p1.lng, p2.lat, p2.lng) / 1000.0, 0) AS vincenty_km
FROM {{zone_name}}.maritime.ports p1 CROSS JOIN {{zone_name}}.maritime.ports p2
WHERE p1.port_id = 1 AND p2.port_id = 2;

-- Navigation bearings
SELECT p1.port_name || ' -> ' || p2.port_name AS route,
       ROUND(st_bearing      (p1.lat, p1.lng, p2.lat, p2.lng), 2) AS bearing_deg,
       ROUND(st_azimuth      (p1.lat, p1.lng, p2.lat, p2.lng), 4) AS azimuth_rad,
       ROUND(st_final_bearing(p1.lat, p1.lng, p2.lat, p2.lng), 2) AS final_bearing_deg
FROM {{zone_name}}.maritime.ports p1 CROSS JOIN {{zone_name}}.maritime.ports p2
WHERE (p1.port_id, p2.port_id) IN ((1,3),(2,10),(5,1),(8,4))
ORDER BY p1.port_id;

-- Point-in-polygon, which vessel positions are inside a harbor?
SELECT pos.position_id, pos.vessel_id, v.vessel_name, p.port_name, pos.lat, pos.lng
FROM {{zone_name}}.maritime.positions pos
JOIN  {{zone_name}}.maritime.vessels  v ON v.vessel_id = pos.vessel_id
CROSS JOIN {{zone_name}}.maritime.ports p
WHERE st_contains(p.harbor_wkt, pos.lat, pos.lng) = true
ORDER BY pos.position_id;

-- Coordinate round-trip
SELECT port_id, port_name,
       st_make_point(lng, lat) AS wkt_point,
       st_x(st_make_point(lng, lat)) AS extracted_lng,
       st_y(st_make_point(lng, lat)) AS extracted_lat
FROM {{zone_name}}.maritime.ports ORDER BY port_id;

-- Harbor areas: Singapore (equator, larger) vs Hamburg (53N, smaller)
SELECT MAX(CASE WHEN port_id = 1 THEN ROUND(st_area(harbor_wkt)/1e6, 2) END) AS area_km2_singapore,
       MAX(CASE WHEN port_id = 6 THEN ROUND(st_area(harbor_wkt)/1e6, 2) END) AS area_km2_hamburg
FROM {{zone_name}}.maritime.ports;

-- Nearest port to each vessel's last at-sea position (st_distance + ROW_NUMBER)
WITH last_sea AS (
    SELECT pos.vessel_id, pos.lat, pos.lng
    FROM {{zone_name}}.maritime.positions pos
    WHERE pos.speed_knots > 0
      AND pos.position_id = (SELECT MAX(p2.position_id)
                             FROM {{zone_name}}.maritime.positions p2
                             WHERE p2.vessel_id = pos.vessel_id AND p2.speed_knots > 0)
),
distances AS (
    SELECT v.vessel_name, ls.lat AS vessel_lat, ls.lng AS vessel_lng,
           p.port_name,
           ROUND(st_distance(ls.lat, ls.lng, p.lat, p.lng) / 1000.0, 0) AS dist_km,
           ROW_NUMBER() OVER (PARTITION BY v.vessel_id
                              ORDER BY st_distance(ls.lat, ls.lng, p.lat, p.lng)) AS rn
    FROM {{zone_name}}.maritime.vessels v
    JOIN last_sea ls ON ls.vessel_id = v.vessel_id
    CROSS JOIN {{zone_name}}.maritime.ports p
)
SELECT vessel_name, vessel_lat, vessel_lng,
       port_name AS nearest_port, dist_km AS nearest_dist_km
FROM distances WHERE rn = 1 ORDER BY vessel_name;

-- ============================================================================
-- CLEANUP
-- ============================================================================
DROP DELTA TABLE IF EXISTS {{zone_name}}.maritime.positions WITH FILES;
DROP DELTA TABLE IF EXISTS {{zone_name}}.maritime.vessels   WITH FILES;
DROP DELTA TABLE IF EXISTS {{zone_name}}.maritime.ports     WITH FILES;
DROP SCHEMA IF EXISTS {{zone_name}}.maritime;

Description

## When to Use Reach for this demo when onboarding to DeltaForge's PostGIS-compatible geometry layer, it is the systematic reference that exercises every st_* function in isolation against independently computed geodesic ground-truth values. Use it to verify that a new storage backend, a new engine build, or a custom function pack returns the expected meters, bearings, and containment results before trusting layered analytics. ## What You Will Learn 1. How to compute intercontinental great-circle distances with `st_distance` (haversine) and compare against `st_distance_sphere` and `st_distance_vincenty` (WGS84 ellipsoid) 2. How to read initial and final compass bearings with `st_bearing`, `st_azimuth` (radians), and `st_final_bearing` 3. How to detect vessels inside a harbor using `st_contains(harbor_wkt, lat, lng)` and its inverse `st_within(lat, lng, harbor_wkt)` 4. How `st_make_point(lng, lat)` round-trips through `st_x` and `st_y` for coordinate construction/extraction 5. How `st_area` on a geodesic polygon shrinks with increasing latitude even when the lat/lng span is fixed (Singapore vs Hamburg at the same 0.02° × 0.02° box) 6. How to combine `st_distance` with a correlated subquery and `ROW_NUMBER()` to find each vessel's nearest port from its last at-sea position ## Prerequisites - DeltaForge GUI running with a workspace open - Write access to an EXTERNAL zone and permission to CREATE SCHEMA - Variables: `{{zone_name}}` (defaults to `external`), `{{data_path}}` for three Delta tables (`ports`, `vessels`, `positions`)

Pitfalls

See Also

Open in interactive docs →   DeltaForge home →