Unit of return value of ST_Distance - postgresql

I need to calculate the distance between
all buildings and
all hospitals
in a map imported from OSM.
I use following query:
SELECT building_id, hospital_id, ST_Distance(building_centroid, hospital_location)
FROM
(
select planet_osm_polygon.osm_id building_id, ST_Centroid(planet_osm_polygon.way) building_centroid
from planet_osm_polygon
where building = 'yes'
) buildings,
(
select planet_osm_point.osm_id hospital_id, planet_osm_point.way hospital_location
from planet_osm_point
where amenity = 'hospital') hospitals
I get strange results - the distance is always smaller than 1.
How can I get the to know the unit, in which these values are reported?
Update 1: Sample result:
Update 2: This query seems to work
SELECT building_id, hospital_id, ST_Distance_sphere(building_centroid, hospital_location) distance
FROM
(
select planet_osm_polygon.osm_id building_id, ST_Centroid(planet_osm_polygon.way) building_centroid
from planet_osm_polygon
where building = 'yes'
) buildings,
(
select planet_osm_point.osm_id hospital_id, planet_osm_point.way hospital_location
from planet_osm_point
where amenity = 'hospital') hospitals
ORDER BY distance

The general rule for units is that the output length units are the same as the input length units.
The OSM way geometry data has length units of degrees of latitude and longitude (SRID=4326). Therefore, the output units from ST_Distance will also have lenth units of degrees, which are not really useful.
There are several things you can do:
Use ST_Distance_Sphere for fast/approximate distances in metres
Use ST_Distance_Spheroid for accurace distances in metres
Convert the lat/long geometry data types to geography, which automagically makes ST_Distance and other functions to use linear units of metres

Related

How to display LAT/LONG of polygon?

I have a table with geom field (type: geometry)
I want to display the values of the polygon in lat/long.
How can I do it ?
I saw this post:
https://gis.stackexchange.com/questions/95373/convert-geometry-to-latitude-longitude-using-postgis-st-transform/95376
but the functions ST_X, ST_Y only works on POINT and not on POLYGON.
How can I display the geom field (which contains polygon) in lat/long format ?
Depending on the output you want, it might very well be that ST_ASGEOJSON already does everything you want, but otherwise, you can use ST_DUMPPOINTS to get all the individual points of the polygon.
SELECT polygon.id,
( SELECT ARRAY_AGG(ST_Y(geom ORDER BY path))
FROM ST_DUMPPOINTS(polygon) AS points ) as latitudes,
( SELECT ARRAY_AGG(ST_X(geom ORDER BY path))
FROM ST_DUMPPOINTS(polygon) AS points ) as longitudes
FROM polygons
would return ordered lists of latitudes and longitudes per polygon.
If you want to combine, them, use JSON:
SELECT polygon.id,
( SELECT ARRAY_AGG(
JSON_BUILD_OBJECT(
'latitude', ST_Y(geom),
'longitude', ST_X(geom)
) ORDER BY path)
)
FROM ST_DUMPPOINTS(polygon) AS points )
FROM polygons

PostGIS Query always brings back all results

I'm playing with PostGIS for the first time and I'm getting all the results back regardless of the distance I use on a distance query. My data looks like this:
Id GeoLocation
8eb63480-4d63-11ea-b06a-8c1645ef6ad2 POINT (52.6323202 1.2947649)
a0f2dde6-4d64-11ea-b06a-8c1645ef6ad2 POINT (52.6294342 1.2936336)
a0f2dde7-4d64-11ea-b06a-8c1645ef6ad2 POINT (52.6277909 1.2909079)
a0f2dde8-4d64-11ea-b06a-8c1645ef6ad2 POINT (52.6260535 1.2952051)
And when I run a query for a point that should be over a mile away:
SELECT * FROM "Locations" WHERE ST_DWithin("GeoLocation", 'POINT(52.6219322 1.2630061)', 1);
I get all of the rows back. My understanding is that the distance parameter should be in metres, so I shouldn't get any results back.
Could it be coordinate format issue? What am I missing?
Using parameters of type geography you get the returned distance in meters, therefore you need to convert it to miles in case you prefer to work with this unit of measurement. If you can cope with degrees, just stick to geometry.
WITH locations (geolocation) AS (
VALUES ('POINT (52.6323202 1.2947649)'),
('POINT (52.6294342 1.2936336)'),
('POINT (52.6277909 1.2909079)'),
('POINT (52.6260535 1.2952051)')
)
SELECT *
FROM locations
WHERE ST_DWithin(
geoLocation::geography,
'POINT(52.6219322 1.2630061)'::geography, 1609*2.2) ;
geolocation
------------------------------
POINT (52.6294342 1.2936336)
POINT (52.6277909 1.2909079)
(2 Zeilen)
EDIT: #JGH pointed out that ST_Distance does not use a spatial index and my previous suggestion was to use it instead of ST_DWithin. It means I was wrong with my preference for ST_Distance :) Here is anyway how to achieve similar results with ST_Distance for those still willing to use it:
WITH locations (geolocation) AS (
VALUES ('POINT (52.6323202 1.2947649)'),
('POINT (52.6294342 1.2936336)'),
('POINT (52.6277909 1.2909079)'),
('POINT (52.6260535 1.2952051)')
)
SELECT *
FROM locations
WHERE ST_Distance(
geoLocation::geography,
'POINT(52.6219322 1.2630061)'::geography) * 0.000621371 > 2.2 ;
geolocation
------------------------------
POINT (52.6323202 1.2947649)
POINT (52.6260535 1.2952051)
(2 Zeilen)
Further reading: Getting all Buildings in range of 5 miles from specified coordinates
Since these seem to be coordinates in longitude and latitude, you should use the geography data type.

Calculate the trajectories of each ship based on an AIS dataset

I have a table of kinematic data (position reports from vessels). The table has the following rows:
row name: type: Description:
timestamp double precision timestamp in UNIX epochs (seconds from 1/1/1970)
type integer AIS message type
mmsi integer MMSI identifier for vessel
status integer Navigational status
lon double precision Longitude (georeference: WGS 1984)
lat double precision Latitude (georeference: WGS 1984)
heading integer True heading in degrees (0-359), relative to true north
turn double precision Rate of turn, right or left, 0 to 720 degrees per minute
speed double precision Speed over ground in knots (allowed values: 0-102.2 knots)
course double precision Course over ground (allowed values: 0-359.9 degrees).
After I imported the data, I added one more row of type geometry formed using the lon, lat and timestamp rows:
SELECT AddGeometryColumn ('public', 'gis_may', 'geom_time', 4326, 'POINTZ', 3);
UPDATE public.gis_may SET geom_time = ST_Transform(ST_SetSRID(ST_MakePoint(lon, lat, gis_may.timestamp ), 4326),4326);
I have to calculate the trajectories of every ship, based on this data. I tried using the code below:
CREATE TABLE ship_trajectories AS SELECT st_makeline(st_setsrid(st_makepoint(lon::REAL, lat::REAL, gis_may.timestamp), 4326)) as traj
FROM gis_may
GROUP BY mmsi;
…but the problem is, this code returns only one linestring/trajectory based only on the coordinates of each ship and does not take into account the times the ship stopped moving.
I think the solution to this is using the row: status (I searched and found what navigational status is: https://help.marinetraffic.com/hc/en-us/articles/203990998-What-is-the-significance-of-the-AIS-Navigational-Status-Values-). When status is 0 it means the ship is moving and when it is 1 it means it is anchored so it no longer forms a trajectory.
I have worked on a project which solved the same problem by first looking at all AIS-points to determine trajectory breaking-points. If you think that the AIS-variable status is enough for your purposes, then good for you. You could check each instance of status changing to 0. Be aware that AIS-data has other status values too. See this.
What if, within each mmsi, you would like to mark the row when a ship goes from status 1 to status 0. This would create the points of delimitation for your trajectories to begin with. Look at this after first adding your longitude-latitude coordinations as a point geometry called gis_point:
-- Make a point geometry and work with those.
SELECT AddGeometryColumn('public', 'gis_may', 'gis_point', 4326, 'POINT', 2);
UPDATE gis_may SET gis_point = ST_Transform(ST_SetSRID(
ST_MakePoint(longitude, latitude),
4326), 4326);
-- Look ahead to keep stuff within the same mmsi. Create a switch for when the status changes:
CREATE TABLE lookahead AS
SELECT aislag.mmsi, aislag.timestamp1, aislag.gis_point, EXTRACT(EPOCH FROM (aislag.ts2 - aislag.ts1)) AS timediff, aislag.g,
CASE WHEN aislag.status = '1' AND aislag.newstatus ='0' THEN 'Start' ELSE '.' END AS newtrajectory
FROM
(SELECT mmsi, timestamp as timestamp1, status, gis_point,
LEAD(mini_c.timestamp) OVER (ORDER BY mmsi, timestamp) AS timestamp2,
LEAD(mini_c.mmsi) OVER (ORDER BY mmsi, timestamp) AS mmsi2,
mini_c.status AS status,
LEAD(mini_c.status) OVER (ORDER BY mmsi, timestamp) AS newstatus
FROM mini_c) AS aislag
WHERE aislag.mmsi = aislag.mmsi2
I've added a lagged time-variable too. Maybe you would like to enlarge the status-switcher to include observations with obsene time-differences to next-following AIS-point for that same ship. Now that you have your delimiters, you can create a new id for rows in lookahead which follows the mmsi, ts-sorting. Then make a new table trajectories with pairs of those ids as id1 and id2 from a lagged selection on lookahead WHERE newstatus = 'Start' so that you get a row for each distance between switches of status. That table basically has trajectories as observations. Joining on the lookahead you can make your line-geometries per trijectory with ST_Makeline(gis_point) using WHERE newid BETWEEN trajectories.is1 AND trajectories.id2 and GROUP BY mmsi.
Sorry for not writing out all the code.
A more advanced approach is to use the actual AIS-data to look at rolling average speeds and distances over time for each mmsi to determine where the ship appears to be standing still for longer periods. The logic above to distinguish delimiters for trajectories would than apply in the same way, only that the AIS-variable switch of status is exchanged for a calculation of behavioral patterns of ships.
Best of luck

PostgreSQL select query to extract latitude and longitude from a point

What SELECT query should be used to extract latitude and longitude from a point?
I cannot use PostGIS.
Example point (point type value) stored in the database:
my_point
--------------
(50.850,4.383)
Expected result after executing the query:
lat | lng
---------------
50.850 | 4.383
The query below works fine but it does not look efficient.
SELECT
split_part(trim(my_point::text, '()'), ',', 1)::float AS lat,
split_part(trim(my_point::text, '()'), ',', 2)::float AS lng
FROM my_table;
Always Read The Fine Manuals
It is possible to access the two component numbers of a point as though the point were an array with indexes 0 and 1. For example, if t.p is a point column then SELECT p[0] FROM t retrieves the X coordinate and UPDATE t SET p1 = ... changes the Y coordinate. In the same way, a value of type box or lseg can be treated as an array of two point values.
Another option would be:
SELECT
ST_X(point) as longitude,
ST_Y(point) as latitude
FROM your_table_name

Finding and Ordering Latitude and Longitude in SQL

I have a SQL database where I store longitude and latitude from an iPhone application. I need to query all the records starting from a given location to the other far most location.
For example, I have longitude x and latitude y. I want all the records first whose longitude matches x the most closely and whose latitude matches y the most closely. I need to all the records one by one in the chain from nearest to farthest. The more distant the location, the greater the value of longitude and latitude will be than x and y.
I hope you got the point and I am waiting for the answer.
Distance with latitude and longitude is not a simple calculation, but one requiring spherical trigonometry.
acos(cos(lat1)*cos(lon1)*cos(lat2)*cos(lon2) +
cos(lat1)*sin(lon1)*cos(lat2)*sin(lon2) +
sin(lat1)*sin(lat2)) * R(adius of the earth)
So this query
select locID, locName, locDesc, lat, lon, locDiffMeters
from (select locID, locName, locDesc, lat, lon,
acos(cos($lat)*cos($lon)*cos(lat)*cos(lon) +
cos($lat)*sin($lon)*cos(lat)*sin(lon) +
sin($lat)*sin(lat) ) * 6,371,000 -- earths radius in meters
as locDiffMeters
from locationTable
where locID <> $ID
) a
order by locDiffMeters
Is probably the right answer, assuming you have that capable of a math library.
Similar to Fosco, but using Pythagoras' Theorem:
select locID, locName, locDesc, lat, lon, locDiff from
(select locID, locName, locDesc, lat, lon,
sqrt((lat - $LAT)*(lat - $LAT) + (lon - $LON)*(lon - $LON)) as locDiff
from locationTable
where locID <> $ID) a
order by locDiff
For really large distances (or locations far from the equator) you should ideally use a geodesic.
Assuming that you've queried the start location lat/lon and location ID... I am using $LAT, $LON, and $ID as placeholders:
select locID, locName, locDesc, lat, lon, locDiff
from (
select locID, locName, locDesc, lat, lon, ABS(lat - $LAT) + ABS(lon - $LON) as locDiff
from locationTable
where locID <> $ID
) a
order by locDiff
Hopefully this helps... may not be the most optimized method, but it should be pretty close.
If you don't have to deal with large distances (see Adam's answer), you might consider using PostgreSQL's geometric types and associated functions.
If you're using Postgresql, add the PostGIS extension and check out the ST_Distance_Sphere
ST_Distance_Sphere — Returns minimum distance in meters between two lon/lat geometries. Uses a spherical earth and radius of 6370986 meters. Faster than ST_Distance_Spheroid, but less accurate. PostGIS versions prior to 1.5 only implemented for points.
SELECT round(CAST(ST_Distance_Sphere(ST_Centroid(the_geom), ST_GeomFromText('POINT(-118 38)',4326)) As numeric),2) As dist_meter ...