PostGIS In Action 2nd Edition better be out in print Q1 2015. Can buy draft now.
PostgresQL: Up and Running 2nd Edition Just went to print.
PostGIS Cookbook (covers up to 2.1)
Prerequisites
Spatially enable a database
Primer on kinds of PostGIS spatial types
Load Spatial Data
Geometry Queries
Geography Queries
Tiger Geocoder
Raster
Slides posted at http://bit.ly/1zfXMhK
Refer to our quick start guide if missing any PostGIS 2.1 install guide
You need PostgreSQL 9.3 or higher.
You need PostGIS 2.1 installed in your PostgreSQL instance.
CREATE ROLE avidgeo LOGIN PASSWORD 'g3o' INHERIT CREATEDB;
CREATE DATABASE avidgeo WITH ENCODING='UTF8' OWNER=avidgeo;
ALTER DATABASE avidgeo SET search_path=public,postgis,contrib;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON TABLES TO avidgeo WITH GRANT OPTION;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON SEQUENCES TO avidgeo WITH GRANT OPTION;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON FUNCTIONS TO avidgeo WITH GRANT OPTION;
We like to install PostGIS in its own schema call postgis and other extensions in a schema called contrib. To run these commands, use pgAdmin or psql. Make sure to connect to the database first.
(only super users can install C-based extensions so need to login as a superuser like postgres)
From pgAdmin or psql
CREATE SCHEMA postgis;
GRANT ALL ON SCHEMA postgis TO public;
CREATE SCHEMA contrib;
GRANT ALL ON SCHEMA contrib TO public;
CREATE EXTENSION postgis SCHEMA postgis;
CREATE EXTENSION hstore SCHEMA contrib;
Exit out of psql and reconnect, then run
SELECT postgis_full_version();
Output should look something like
postgis_full_version --------------------------------------------------------------------------------- POSTGIS="2.1.4 r12966" GEOS="3.4.2-CAPI-1.8.2 r3924" PROJ="Rel. 4.8.0, 6 March 2012" GDAL="GDAL 1.11.0, released 2014/04/16" LIBXML="2.7.8" LIBJSON="UNKNOWN" RASTER
DROP TABLE IF EXISTS worlds;
CREATE TABLE worlds (
id serial,
name varchar(20),
geom geometry(POINT),
CONSTRAINT pk_worlds
PRIMARY KEY(id)
);
CREATE INDEX idx_words_geom ON worlds USING gist (geom);
ALTER TABLE worlds ADD COLUMN points geometry(point);
ALTER TABLE worlds ADD COLUMN linestrings geometry(linestring);
ALTER TABLE worlds ADD COLUMN polygons geometry(polygon);
SELECT f_table_name as tbl, f_geometry_column As geoc, srid, type
FROM geometry_columns;
tbl | geoc | srid | type ----------------+-------------+------+------------ raster_columns | extent | 0 | GEOMETRY worlds | geom | 0 | POINT worlds | points | 0 | POINT worlds | linestrings | 0 | LINESTRING worlds | polygons | 0 | POLYGON
TRUNCATE TABLE worlds;
INSERT INTO worlds (name, points)
VALUES
('City Hall', ST_GeomFromText('POINT(0 0)')),
('Fire Station', ST_GeomFromText('POINT(2 4)')),
('Tower', ST_GeomFromText('POINT(10 0)'));
INSERT INTO worlds (name, linestrings)
VALUES ('Main St', ST_GeomFromText('LINESTRING(-12 0,0 0,12 12)'));
INSERT INTO worlds (name, polygons)
VALUES (
'Centerville',
ST_GeomFromText('
POLYGON((-10 -10,-10 10,10 10, 10 -10,-10 -10))'
)
);
ALTER TABLE worlds
ALTER COLUMN geom TYPE geometry
USING(COALESCE(polygons,linestrings,points));
Exactly the same as geometry except use ST_GeogFromText and geography type. Spatial reference system defaults to 4326 (WGS 84 Lon Lat) if not specified. Coordinates always in decimal degrees.
ALTER TABLE worlds ADD COLUMN geog geography;
UPDATE worlds
SET geog = COALESCE(polygons,linestrings,points)::geography;
SELECT f_table_name as tbl, f_geography_column As geoc, srid, type
FROM geography_columns;
tbl | geoc | srid | type --------+------+------+---------- worlds | geog | 0 | Geometry
SELECT name, ST_Area(geom) As deg_aread, ST_Area(Geog) as m_area
FROM worlds
WHERE name ='Centerville';
name | deg_aread | m_area ------------+-----------+------------------ Centerville | 400 | 4969694980894.16
Cataloged in spatial_ref_sys table.
AKA as Lon-Lat in geometry.
Goes by many SRIDs : 900913 (defunct), EPSG:3857, EPSG:3785 (defunct)
We'll talk about getting data, loading data, and querying data.
Office of Geographic Information (MassGIS Datalayers)
Datasets we'll use in Massachusetts State Plane Meters (SRID: 26986)
SELECT srid, srtext, proj4text
FROM spatial_ref_sys
WHERE
srtext ILIKE '%NAD83%' AND
srtext ILIKE '%Massachusetts%' AND
srtext ILIKE '%UNIT["met%",1%';
srid | srtext | proj4text -------+--------------------------------------------------------------+---------------------- 2805 | PROJCS["NAD83(HARN) / Massachusetts Mainland",GEOGCS["NAD83( | +proj=lcc +lat_1=42. 2806 | PROJCS["NAD83(HARN) / Massachusetts Island",GEOGCS["NAD83(HA | +proj=lcc +lat_1=41. 26986 | PROJCS["NAD83 / Massachusetts Mainland",GEOGCS["NAD83",DATUM | +proj=lcc +lat_1=42. 26987 | PROJCS["NAD83 / Massachusetts Island",GEOGCS["NAD83",DATUM[" | +proj=lcc +lat_1=41. 3583 | PROJCS["NAD83(NSRS2007) / Massachusetts Island",GEOGCS["NAD8 | +proj=lcc +lat_1=41. 3585 | PROJCS["NAD83(NSRS2007) / Massachusetts Mainland",GEOGCS["NA | +proj=lcc +lat_1=42.
This is generally more of an issue on windows especially since you might have multiple versions of PostgreSQL.
SET PATH=%PATH%;"C:\Program Files\PostgreSQL\9.3\bin"
Useful for loading data using shp2pgsql or raster2pgsql
Windows users replace export with SET
export PATH=...
export PGPORT=5432
export PGHOST=localhost
export PGUSER=postgres
export PGPASSWORD=whatever
export PGDATABASE=avidgeo
-g column_name, -I index, -s spatial_ref sys, -S single, -D copy mode
shp2pgsql -s 26986 -D -g geom -I -W "latin1" "MBTA_ARC" mbta_arc | psql
shp2pgsql -s 26986 -D -I -W "latin1" "MBTA_NODE" mbta_node | psql
shp2pgsql -s 26986 -D -I "MBTABUSSTOPS_PT" mbta_bus_stops | psql
shp2pgsql -s 26986 -D -I -S "MBTABUSROUTES_ARC" mbta_bus_routes | psql
-g column_name, -G force geography, -I index, -s spatial_ref sys. You can't use -D if you will transform -s from:to
. So it's a bit slower.
shp2pgsql -s 26986:4326 -G -I -W "latin1" "TOWNS_POLY" towns | psql
ALTER TABLE towns ADD COLUMN geom geometry (MULTIPOLYGON, 26986);
UPDATE towns SET geom = ST_Transform(geog::geometry, 26986);
CREATE INDEX idx_towns_geom ON towns USING gist(geom);
SELECT * FROM geometry_columns;
SELECT * FROM geography_columns;
Units are always in the units of our spatial reference system. For our Massachusetts data that is state-plane meters, our units are in meters.
Use ST_DWithin
WITH ref As (
SELECT
ST_Transform(
ST_GeomFromText(
'POINT(-71.0824 42.3659)', 4326
),
26986
) As loc
)
SELECT n.station, n.route, ST_Distance(n.geom, ref.loc) As dist_m
FROM
mbta_node As n
INNER JOIN
ref
ON ST_DWithin(n.geom, ref.loc, 1000)
ORDER BY dist_m;
station | route | dist_m -------------+---------------------------------------+------------------ Kendall/MIT | A - Ashmont B - Braintree C - Alewife | 497.356410111099 Lechmere | E - Lechmere | 705.062362967058
Use KNN distance operators
WITH ref As (
SELECT
ST_Transform(
ST_GeomFromText(
'POINT(-71.0824 42.3659)', 4326
),
26986
) As loc
)
SELECT
n.station,
n.route,
ST_Distance(
n.geom,
(SELECT loc FROM ref)
)::numeric(10,2) As dist_m,
(n.geom <-> (SELECT loc FROM ref))::numeric(10,2) As dist_knn
FROM mbta_node As n
ORDER BY n.geom <-> (SELECT loc FROM ref)
LIMIT 3;
station | route | dist_m | dist_knn ------------+-----------------------------------------+---------+--------- Kendall/MIT | A - Ashmont B - Braintree C - Alewife | 497.36 | 497.39 Lechmere | E - Lechmere | 705.06 | 705.08 Charles/MGH | A - Ashmont B - Braintree C - Alewife | 1070.40 | 1070.39
Use ST_Length
SELECT mbta_route, SUM(ST_Length(geom)) As len_m
FROM mbta_bus_routes
GROUP BY mbta_route
ORDER BY len_m DESC LIMIT 5;
mbta_route | len_m ------------+------------------ 34E | 440190.649593145 134 | 213819.732897472 354 | 196239.007361261 240 | 184848.966892802 435 | 177963.889656812
We'll use osm2pgsql for loading since it works on windows as well and easiest to compile on other OS.
Binaries available: Osm2pgsql. If on windows use the one marked Cygwin.
Open Street Map data usually loaded in web mercator
hstore needs to be installed before you can use --hstore-all or --hstore
-W switch prompts for password. if you are missing the default.style file get from here
osm2pgsql
-d avidgeo -H localhost -U postgres -P 5432 -W -S default.style
--hstore boston_massachusetts.osm.pbf
CREATE INDEX idx_planet_osm_point_fgeom ON planet_osm_point USING gist( ( ST_Transform(way,26986)::geometry(POINT,26986) ) ); CREATE INDEX idx_planet_osm_point_fgeog ON planet_osm_point USING gist( ( ST_Transform(way,4326)::geography(POINT,4326) ) ); CREATE OR REPLACE VIEW vw_planet_osm_point AS SELECT *, ST_Transform(way,26986)::geometry(POINT,26986) As geom, ST_Transform(way,4326)::geography(POINT,4326) As geog FROM planet_osm_point;
WITH loc as ( SELECT ST_GeogFromText('POINT(-71.0824 42.3659)') As geog )
SELECT name, tags->'cuisine' As cuisine,
ST_Distance(pt.geog, loc.geog) As dist
FROM vw_planet_osm_point AS pt INNER JOIN loc ON
ST_Dwithin(pt.geog, loc.geog, 500)
AND pt.tags ? 'cuisine'
ORDER BY dist Limit 5;
name | cuisine | dist ------------------------+-------------+--------------- Fuji at Kendall | japanese | 70.455413701 Tatte Bakery and CafΘ | bakery | 109.979903628 Voltage | coffee_shop | 183.5050868 Kika Tapas | regional | 281.472198034 Commonwealth Cambridge | american | 288.972055815
--takes 230 ms, replacing geog with geom reduces to 22 ms
SELECT pt.osm_id, pt.name,
tags->'cuisine' As cuisine, amenity, pt.geog
FROM vw_planet_osm_point AS pt INNER JOIN towns As t
ON (t.town = 'CAMBRIDGE' AND ST_Covers(t.geog, pt.geog))
WHERE (pt.tags ? 'cuisine'
or amenity IN('restaurant', 'fast_food', 'cafe', 'pub')) ;
Respects Postgres environment variables PGHOST, PGPORT, PGUSER or takes them as switch inputs
-g to specify geometry column. only needed if more than one
pgsql2shp -f pois -g geom avidgeo vw_planet_osm_point
SQL should be quoted and all on same line
pgsql2shp -u avidgeo -P g3o -p 5432 -f cuisine avidgeo
"SELECT pt.name, pt.geom, pt.tags->'cuisine'::varchar(50) AS cuisine
FROM vw_planet_osm_point AS pt WHERE pt.tags ? 'cuisine'"
PostGIS 2.2 dev (trunk) been upgraded to use newly released Tiger 2014 dataset. You can use the postgis_tiger_geocoder extension files. Copy it into your PostgreSQL from Windows Builds, even if on Linux.
CREATE EXTENSION fuzzystrmatch SCHEMA contrib;
CREATE EXTENSION address_standardizer SCHEMA contrib; -- optional
CREATE EXTENSION postgis_tiger_geocoder;
Usually the first step before geocoding. TIGER without street data can normalize in most cases, but not all.
SELECT na.address, na.streetname, na.streettypeabbrev, na.zip
FROM normalize_address('275 Third St, Cambridge, MA, 02142') AS na;
address | streetname | streettypeabbrev | zip ---------+------------+------------------+------- 275 | Third | St | 02142
Create a folder called gisdata and within that temp
These are used for staging. Edit the tiger.loader_variables.staging_fold to have path to location.
Here I use windows. If on Linux or Mac use sh.
INSERT INTO tiger.loader_platform (
os, declare_sect, pgbin, wget, unzip_command, psql, path_sep, loader,
environ_set_command, county_process_command
)
SELECT
'mydb', declare_sect, pgbin, wget, unzip_command, psql, path_sep,
loader, environ_set_command, county_process_command
FROM tiger.loader_platform
WHERE os = 'windows';
Use pgAdmin or favorite Postgres admin tool to edit the declare_sect.
\a \t \o c:/gisdata/nation_script.bat
SELECT Loader_Generate_Nation_Script('mydb');
\o
Then run the generated script from psql command line.
If you don't need tab blocks, block groups, and tracts for statistics, don't load.
UPDATE tiger.loader_lookuptables
SET load = false
WHERE table_name IN ('bg','tabblock','tract');
\a \t \o c:/gisdata/state_script.bat
SELECT Loader_Generate_Script('{MA}'::text[], 'mydb');
\o
SELECT cntyidfp, name, namelsad
FROM county
WHERE ST_Intersects(
the_geom,
ST_GeomFromText('POINT(-71.11678 42.36587)',4269)
);
cntyidfp | name | namelsad ----------+-----------+------------------ 25017 | Middlesex | Middlesex County
If you don't need a whole state of data, edit the generated script.
In wget calls for faces, edges, featnames, addr, change 25 to 25017 (if just want portion of Middlesex county).
Then run the generated script from command line.
SELECT Install_Missing_Indexes();
Don't forget to update stats.
analyze verbose;
SELECT pprint_addy(addy), ST_AsText(ST_SnapToGrid(geomout,0.0001)), rating
FROM geocode('275 3rd St, Cambridge, MA, 02142',1);
pprint_addy | st_astext | rating ---------------------------------+-------------------------+-------- 275 3rd St, Cambridge, MA 02142 | POINT(-71.0824 42.3659) | 0
Puts us at the door. Google geocodes to 42.365946, -71.082573 which puts us inside the building.
Intersection name on 3rd St
SELECT
pprint_addy(addy),
ST_AsText(ST_SnapToGrid(geomout,0.0001)) As loc,
rating
FROM geocode_intersection('3rd St','Binney St','MA','Cambridge');
pprint_addy | loc | rating --------------------------------+-------------------------+-------- 282 3rd St, Cambridge, MA 02141 | POINT(-71.0824 42.3657) | 0 281 3rd St, Cambridge, MA 02142 | POINT(-71.0824 42.3657) | 0 280 3rd St, Cambridge, MA 02141 | POINT(-71.0824 42.3657) | 0 279 3rd St, Cambridge, MA 02142 | POINT(-71.0824 42.3657) | 0
Intersection name on Binney St
SELECT
pprint_addy(addy),
ST_AsText(ST_SnapToGrid(geomout,0.0001)) As loc,
rating
FROM geocode_intersection('Binney St','3rd St','MA','Cambridge');
pprint_addy | loc | rating -----------------------------------+-------------------------+-------- 148 Binney St, Cambridge, MA 02142 | POINT(-71.0824 42.3657) | 0 149 Binney St, Cambridge, MA 02143 | POINT(-71.0824 42.3657) | 0 146 Binney St, Cambridge, MA 02142 | POINT(-71.0824 42.3657) | 0 147 Binney St, Cambridge, MA 02142 | POINT(-71.0824 42.3657) | 0
You get better performance if you provide zip and max returns
SELECT
pprint_addy(addy),
ST_AsText(ST_SnapToGrid(geomout,0.00001)) As loc,
rating
FROM geocode_intersection('Binney St','3rd St','MA','Cambridge','02142',1);
pprint_addy | loc | rating -----------------------------------+---------------------------+-------- 148 Binney St, Cambridge, MA 02142 | POINT(-71.08239 42.36575) | 0
addy is an array of possible normalized addresses of a point location. You can expand aliases with unnest.
SELECT pprint_addy(unnest(rc.addy))
FROM reverse_geocode(ST_Point(-71.0824, 42.3659)) As rc;
pprint_addy ---------------------------------- 277 3rd St, Cambridge, MA 02142 151 Binney St, Cambridge, MA 02143
Download some raster data
Unzip and extract into folder
raster2pgsql -s 26986 -F -I -C -M wind/ne_spd_30m -t 200x200 wind_spd_30m | psql -d avidgeo
Aerials are in UTM Zone 10 NAD 83 meters SRID:26919
Load and generate -I add spatial index, -l overviews (really speeds up display in QGIS), -Y use copy, -e no transaction, -F include filename, -C add constraints, -t tile size, -M analyse (update planner stats)
PostGIS 2.1 raster2pgsql can guess at the SRID if enough info in meta data. So you can skip the SRID.
raster2pgsql -Y -e -F -I -C -M aerials/*.jpg -t 500x500 -l 2,4 aerials | psql -d avidgeo
SELECT ST_Value(rast, 1, geom) As wdval
FROM
wind_spd_30m As w
INNER JOIN
ST_Transform(
ST_GeomFromText('POINT(-71.082573 42.365946)',4326),26986
) As geom
ON ST_Intersects(w.rast, geom);
wdval ---------------- 5.16392469406128
500 meter box around point of interest, 3 bins
SELECT (h).*
FROM (
SELECT ST_Histogram(ST_Union(ST_Clip(rast,geom)),1,3) As h
FROM
wind_spd_30m As w
INNER JOIN
ST_Expand(
ST_Transform(
ST_GeomFromText('POINT(-71.082573 42.365946)',4326),
26986
),
300
) As geom
ON ST_Intersects(w.rast,geom)
) AS c;
min | max | count | percent -----------------+------------------+-------+------------------ 4.92105531692505 | 5.04267676671346 | 3 | 0.333333333333333 5.04267676671346 | 5.16429821650187 | 2 | 0.222222222222222 5.16429821650187 | 5.28591966629028 | 4 | 0.444444444444444
This took 2781 ms
SELECT
ST_AsPNG(
ST_Resize(ST_Union(ST_Clip(w.rast,geom)),0.5,0.5)
) As png
FROM
aerials AS w
INNER JOIN
ST_Buffer(
ST_Transform(
ST_GeomFromText('POINT(-71.0824 42.3659)',4326),
26919
),
150
) As geom
ON ST_Intersects(w.rast,geom);
Using overview is much faster (o2 = ~921 ms, o4 = ~270ms). o2 achieves same result.
SELECT ST_AsPNG(ST_Union(ST_Clip(w.rast, geom))) As png
FROM
o_2_aerials AS w
INNER JOIN
ST_Buffer(
ST_Transform(
ST_GeomFromText('POINT(-71.0824 42.3659)',4326),
26919
),
150
) As geom
ON ST_Intersects(w.rast,geom);
CREATE OR REPLACE VIEW aerial_test AS
SELECT 1 As rid, ST_Union(ST_Clip(w.rast, geom)) As rast
FROM
o_2_aerials AS w INNER JOIN
ST_Buffer(
ST_Transform(
ST_GeomFromText('POINT(-71.0824 42.3659)', 4326),
26919
),
150
) As geom
ON ST_Intersects(w.rast,geom);
Then in DBManager drag the view onto map.