[postgis-tickets] [SCM] PostGIS branch master updated. 3.1.0rc1-144-gdabd772
git at osgeo.org
git at osgeo.org
Mon Apr 19 12:35:59 PDT 2021
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "PostGIS".
The branch, master has been updated
via dabd7723201a2efed06fd3d3e77c35e5cec7e44b (commit)
from f6e693915a819d82471124ef938e3d45167137a4 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit dabd7723201a2efed06fd3d3e77c35e5cec7e44b
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date: Mon Apr 19 12:34:21 2021 -0700
Add functions ST_SetZ()/ST_SetM()
that fill in z/m coordinates of a geometry using data read
from a raster. Also adds 'bilinear' interpolation mode
to ST_Value() to allow sampling between pixes in reading
values from rasters.
diff --git a/NEWS b/NEWS
index 2d92e2d..bfc48bc 100644
--- a/NEWS
+++ b/NEWS
@@ -10,11 +10,21 @@ PostGIS 3.2.0
(Sandro Santilli)
- #4827, Allow NaN coordinates in WKT input (Paul Ramsey)
- #4870, Allow open options to be passed to GDAL drivers (Paul Ramsey)
+ - ST_Value() accepts resample parameter to add bilinear option (Paul Ramsey)
- #3778, #4401, ST_Boundary now works for TIN and does not linearize curves (Aliaksandr Kalenik)
* New features*
- #4841, FindTopology to quickly get a topology record (Sandro Santilli)
- #4851, TopoGeom_addTopoGeom function (Sandro Santilli)
+ - ST_InterpolateRaster() fills in raster cells between sample points
+ using one of a number of algorithms (inverse weighted distance, average, etc)
+ using algorithms from GDAL
+ (Paul Ramsey)
+ - ST_Contour() generates contour lines from raster values
+ using algorithms from GDAL (Paul Ramsey)
+ - ST_SetZ()/ST_SetM() fills in z/m coordinates of a geometry using data read
+ from a raster (Paul Ramsey)
+
PostGIS 3.1.0
2020/12/18
diff --git a/doc/reference_raster.xml b/doc/reference_raster.xml
index e824bf6..b0f5a45 100644
--- a/doc/reference_raster.xml
+++ b/doc/reference_raster.xml
@@ -5260,6 +5260,151 @@ FROM (
</refentry>
+ <refentry id="RT_ST_SetZ">
+ <refnamediv>
+ <refname>ST_SetZ</refname>
+ <refpurpose>Returns a geometry with the same X/Y coordinates as the input geometry, and values from the raster copied into the Z dimension using the requested resample algorithm.</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <funcsynopsis>
+ <funcprototype>
+ <funcdef>geometry <function>ST_GetValues</function></funcdef>
+ <paramdef><type>raster </type> <parameter>rast</parameter></paramdef>
+ <paramdef><type>geometry </type> <parameter>geom</parameter></paramdef>
+ <paramdef choice="opt"><type>text </type> <parameter>resample=nearest</parameter></paramdef>
+ <paramdef choice="opt"><type>integer </type> <parameter>band=1</parameter></paramdef>
+ </funcprototype>
+ </funcsynopsis>
+ </refsynopsisdiv>
+
+ <refsection>
+ <title>Description</title>
+
+ <para>Returns a geometry with the same X/Y coordinates as the input geometry, and values from the raster copied into the Z dimensions using the requested resample algorithm.</para>
+ <para>The <varname>resample</varname> parameter can be set to "nearest" to copy the values from the cell each vertex falls within, or "bilinear" to use <ulink url="https://en.wikipedia.org/wiki/Bilinear_interpolation">bilinear interpolation</ulink> to calculate a value that takes neighboring cells into account also.</para>
+ </refsection>
+
+ <refsection>
+ <title>Examples</title>
+ <programlisting>--
+-- 2x2 test raster with values
+--
+-- 10 50
+-- 40 20
+--
+WITH test_raster AS (
+SELECT
+ST_SetValues(
+ ST_AddBand(
+ ST_MakeEmptyRaster(width => 2, height => 2,
+ upperleftx => 0, upperlefty => 2,
+ scalex => 1.0, scaley => -1.0,
+ skewx => 0, skewy => 0, srid => 4326),
+ index => 1, pixeltype => '16BSI',
+ initialvalue => 0,
+ nodataval => -999),
+ 1,1,1,
+ newvalueset =>ARRAY[ARRAY[10.0::float8, 50.0::float8], ARRAY[40.0::float8, 20.0::float8]]) AS rast
+)
+SELECT
+ST_AsText(
+ ST_SetZ(
+ rast,
+ band => 1,
+ geom => 'SRID=4326;LINESTRING(1.0 1.9, 1.0 0.2)'::geometry,
+ resample => 'bilinear'
+))
+FROM test_raster
+
+ st_astext
+----------------------------------
+ LINESTRING Z (1 1.9 38,1 0.2 27)</programlisting>
+
+ </refsection>
+
+ <refsection>
+ <title>See Also</title>
+ <para>
+ <xref linkend="RT_ST_Value" />,
+ <xref linkend="RT_ST_SetM" />
+ </para>
+ </refsection>
+ </refentry>
+
+
+ <refentry id="RT_ST_SetM">
+ <refnamediv>
+ <refname>ST_SetM</refname>
+ <refpurpose>Returns a geometry with the same X/Y coordinates as the input geometry, and values from the raster copied into the Z dimension using the requested resample algorithm.</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <funcsynopsis>
+ <funcprototype>
+ <funcdef>geometry <function>ST_GetValues</function></funcdef>
+ <paramdef><type>raster </type> <parameter>rast</parameter></paramdef>
+ <paramdef><type>geometry </type> <parameter>geom</parameter></paramdef>
+ <paramdef choice="opt"><type>text </type> <parameter>resample=nearest</parameter></paramdef>
+ <paramdef choice="opt"><type>integer </type> <parameter>band=1</parameter></paramdef>
+ </funcprototype>
+ </funcsynopsis>
+ </refsynopsisdiv>
+
+ <refsection>
+ <title>Description</title>
+
+ <para>Returns a geometry with the same X/Y coordinates as the input geometry, and values from the raster copied into the Z dimensions using the requested resample algorithm.</para>
+ <para>The <varname>resample</varname> parameter can be set to "nearest" to copy the values from the cell each vertex falls within, or "bilinear" to use <ulink url="https://en.wikipedia.org/wiki/Bilinear_interpolation">bilinear interpolation</ulink> to calculate a value that takes neighboring cells into account also.</para>
+ </refsection>
+
+ <refsection>
+ <title>Examples</title>
+ <programlisting>--
+-- 2x2 test raster with values
+--
+-- 10 50
+-- 40 20
+--
+WITH test_raster AS (
+SELECT
+ST_SetValues(
+ ST_AddBand(
+ ST_MakeEmptyRaster(width => 2, height => 2,
+ upperleftx => 0, upperlefty => 2,
+ scalex => 1.0, scaley => -1.0,
+ skewx => 0, skewy => 0, srid => 4326),
+ index => 1, pixeltype => '16BSI',
+ initialvalue => 0,
+ nodataval => -999),
+ 1,1,1,
+ newvalueset =>ARRAY[ARRAY[10.0::float8, 50.0::float8], ARRAY[40.0::float8, 20.0::float8]]) AS rast
+)
+SELECT
+ST_AsText(
+ ST_SetM(
+ rast,
+ band => 1,
+ geom => 'SRID=4326;LINESTRING(1.0 1.9, 1.0 0.2)'::geometry,
+ resample => 'bilinear'
+))
+FROM test_raster
+
+ st_astext
+----------------------------------
+ LINESTRING M (1 1.9 38,1 0.2 27)</programlisting>
+
+ </refsection>
+
+ <refsection>
+ <title>See Also</title>
+ <para>
+ <xref linkend="RT_ST_Value" />,
+ <xref linkend="RT_ST_SetZ" />
+ </para>
+ </refsection>
+ </refentry>
+
<refentry id="RT_ST_Neighborhood">
<refnamediv>
<refname>ST_Neighborhood</refname>
diff --git a/raster/rt_core/librtcore.h b/raster/rt_core/librtcore.h
index 97bda9e..8f35f29 100644
--- a/raster/rt_core/librtcore.h
+++ b/raster/rt_core/librtcore.h
@@ -1360,6 +1360,27 @@ rt_errorstate rt_raster_geopoint_to_cell(
double *igt
);
+
+/**
+ * Convert an xw,yw map point to a xr,yr raster point
+ *
+ * @param raster : the raster to get info from
+ * @param xw : X ordinate of the geographical point
+ * @param yw : Y ordinate of the geographical point
+ * @param xr : output parameter, the x ordinate in raster space
+ * @param yr : output parameter, the y ordinate in raster space
+ * @param igt : input/output parameter, inverse geotransform matrix
+ *
+ * @return ES_NONE if success, ES_ERROR if error
+ */
+rt_errorstate rt_raster_geopoint_to_rasterpoint(
+ rt_raster raster,
+ double xw, double yw,
+ double *xr, double *yr,
+ double *igt
+);
+
+
/**
* Get raster's convex hull.
*
@@ -1397,6 +1418,72 @@ rt_errorstate rt_raster_get_envelope(rt_raster raster, rt_envelope *env);
rt_errorstate rt_raster_get_envelope_geom(rt_raster raster, LWGEOM **env);
/**
+ * Retrieve a point value from the raster using a world coordinate
+ * and bilinear interpolation.
+ *
+ * @param band : the band to read for values
+ * @param xr : x unrounded raster coordinate
+ * @param yr : y unrounded raster coordinate
+ * @param r_value : return pointer for point value
+ * @param r_nodata : return pointer for if this is a nodata
+ *
+ * @return ES_ERROR on error, otherwise ES_NONE
+ */
+rt_errorstate rt_band_get_pixel_bilinear(
+ rt_band band,
+ double xr, double yr,
+ double *r_value, int *r_nodata
+);
+
+typedef enum {
+ RT_NEAREST,
+ RT_BILINEAR
+} rt_resample_type;
+
+/**
+ * Retrieve a point value from the raster using a world coordinate
+ * and selected resampling method.
+ *
+ * @param band : the band to read for values
+ * @param xr : x unrounded raster coordinate
+ * @param yr : y unrounded raster coordinate
+ * @param resample : algorithm for reading raster (nearest or bilinear)
+ * @param r_value : return pointer for point value
+ * @param r_nodata : return pointer for if this is a nodata
+ *
+ * @return ES_ERROR on error, otherwise ES_NONE
+ */
+rt_errorstate rt_band_get_pixel_resample(
+ rt_band band,
+ double xr, double yr,
+ rt_resample_type resample,
+ double *r_value, int *r_nodata
+);
+
+/**
+ * Copy values from a raster to the points on a geometry
+ * using the requested interpolation type.
+ * and selected interpolation.
+ *
+ * @param raster : the raster to read for values
+ * @param bandnum : the band number to read from
+ * @param dim : the geometry dimension to copy values into 'Z' or 'M'
+ * @param resample : algorithm for reading raster (nearest or bilinear)
+ * @param lwgeom_in : the input geometry
+ * @param lwgeom_out : pointer for the output geometry
+ *
+ * @return ES_ERROR on error, otherwise ES_NONE
+ */
+rt_errorstate rt_raster_copy_to_geometry(
+ rt_raster raster,
+ uint32_t bandnum,
+ char dim, /* 'Z' or 'M' */
+ rt_resample_type resample,
+ const LWGEOM *lwgeom_in,
+ LWGEOM **lwgeom_out
+);
+
+/**
* Get raster perimeter
*
* The perimeter is a 4 vertices (5 to be closed)
diff --git a/raster/rt_core/rt_band.c b/raster/rt_core/rt_band.c
index 60ca2ce..fb6e656 100644
--- a/raster/rt_core/rt_band.c
+++ b/raster/rt_core/rt_band.c
@@ -1206,6 +1206,160 @@ rt_errorstate rt_band_get_pixel_line(
}
/**
+ * Retrieve a point value from the raster using a world coordinate
+ * and selected interpolation.
+ *
+ * @param band : the band to read for values
+ * @param xr : x unrounded raster coordinate
+ * @param yr : y unrounded raster coordinate
+ * @param r_value : return pointer for point value
+ * @param r_nodata : return pointer for if this is a nodata
+ *
+ * @return ES_ERROR on error, otherwise ES_NONE
+ */
+rt_errorstate
+rt_band_get_pixel_resample(
+ rt_band band,
+ double xr, double yr,
+ rt_resample_type resample,
+ double *r_value, int *r_nodata
+)
+{
+ if (resample == RT_BILINEAR) {
+ return rt_band_get_pixel_bilinear(
+ band, xr, yr, r_value, r_nodata
+ );
+ }
+ else if (resample == RT_NEAREST) {
+ return rt_band_get_pixel(
+ band, floor(xr), floor(yr),
+ r_value, r_nodata
+ );
+ }
+ else {
+ rtwarn("Invalid resample type requested %d", resample);
+ return ES_ERROR;
+ }
+
+}
+
+/**
+ * Retrieve a point value from the raster using a world coordinate
+ * and bilinear interpolation.
+ *
+ * @param rast : the raster to read for values
+ * @param bandnum : the band to read for the values
+ * @param xw : x world coordinate in
+ * @param yw : y world coordinate in
+ * @param r_value : return pointer for point value
+ * @param r_nodata : return pointer for if this is a nodata
+ *
+ * @return ES_ERROR on error, otherwise ES_NONE
+ */
+rt_errorstate
+rt_band_get_pixel_bilinear(
+ rt_band band,
+ double xr, double yr,
+ double *r_value, int *r_nodata)
+{
+ rt_errorstate err;
+ double xcenter, ycenter;
+ double values[2][2];
+ double nodatavalue = 0.0;
+ int nodatas[2][2];
+ int x[2][2];
+ int y[2][2];
+ int xcell, ycell;
+ int xdir, ydir;
+ int i, j;
+ uint16_t width, height;
+
+ /* Cell coordinates */
+ xcell = (int)floor(xr);
+ ycell = (int)floor(yr);
+ xcenter = xcell + 0.5;
+ ycenter = ycell + 0.5;
+
+ /* Raster geometry */
+ width = rt_band_get_width(band);
+ height = rt_band_get_height(band);
+
+ /* Reject out-of-range sample */
+ if(xcell < 0 || ycell < 0 || xcell >= width || ycell >= height) {
+ rtwarn("Attempting to get pixel value with out of range raster coordinates: (%d, %d)", xcell, ycell);
+ return ES_ERROR;
+ }
+
+ /* Quadrant of 2x2 grid the raster coordinate falls in */
+ xdir = xr < xcenter ? 1 : 0;
+ ydir = yr < ycenter ? 1 : 0;
+
+ err = rt_band_get_nodata(band, &nodatavalue);
+ if (err != ES_NONE) {
+ nodatavalue = 0.0;
+ }
+
+ /* Read the 2x2 values from the band */
+ for (i = 0; i < 2; i++) {
+ for (j = 0; j < 2; j++) {
+ double value = nodatavalue;
+ int nodata = 0;
+ int xij = xcell + (i - xdir);
+ int yij = ycell + (j - ydir);
+
+ if(xij < 0 || yij < 0 || xij >= width || yij >= height) {
+ nodata = 1;
+ }
+ else {
+ rt_errorstate err = rt_band_get_pixel(
+ band, xij, yij,
+ &value, &nodata
+ );
+ if (err != ES_NONE)
+ nodata = 1;
+ }
+ x[i][j] = xij;
+ y[i][j] = yij;
+ values[i][j] = value;
+ nodatas[i][j] = nodata;
+ }
+ }
+
+ /* Point falls in nodata cell, just return nodata */
+ if (nodatas[xdir][ydir]) {
+ *r_value = nodatavalue;
+ *r_nodata = 1;
+ return ES_NONE;
+ }
+
+ /* Normalize raster coordinate to the bottom left */
+ /* so we are working on a unit square */
+ xr = xr - (x[0][0] + 0.5);
+ yr = yr - (y[0][0] + 0.5);
+
+ /* Point is in cell with values, so we take nodata */
+ /* neighbors off the table by matching them to the */
+ /* most controlling cell */
+ for (i = 0; i < 2; i++) {
+ for (j = 0; j < 2; j++) {
+ if (nodatas[i][j])
+ values[i][j] = values[xdir][ydir];
+ }
+ }
+
+ /* Calculate bilinear value */
+ /* https://en.wikipedia.org/wiki/Bilinear_interpolation#Unit_square */
+ *r_nodata = 0;
+ *r_value = values[0][0] * (1-xr) * (1-yr) +
+ values[1][0] * (1-yr) * xr +
+ values[0][1] * (1-xr) * yr +
+ values[1][1] * xr * yr;
+
+ return ES_NONE;
+}
+
+
+/**
* Get pixel value. If band's isnodata flag is TRUE, value returned
* will be the band's NODATA value
*
@@ -1637,6 +1791,8 @@ uint32_t rt_band_get_nearest_pixel(
return count;
}
+
+
/**
* Search band for pixel(s) with search values
*
diff --git a/raster/rt_core/rt_raster.c b/raster/rt_core/rt_raster.c
index e75a89b..7ceecba 100644
--- a/raster/rt_core/rt_raster.c
+++ b/raster/rt_core/rt_raster.c
@@ -789,7 +789,7 @@ rt_raster_cell_to_geopoint(
}
/**
- * Convert an xw,yw map point to a xr,yr raster point
+ * Convert an xw,yw map point to a xr,yr cell coordinate
*
* @param raster : the raster to get info from
* @param xw : X ordinate of the geographical point
@@ -807,8 +807,51 @@ rt_raster_geopoint_to_cell(
double *xr, double *yr,
double *igt
) {
- double _igt[6] = {0};
double rnd = 0;
+ rt_errorstate err;
+
+ err = rt_raster_geopoint_to_rasterpoint(raster, xw, yw, xr, yr, igt);
+ if (err != ES_NONE)
+ return err;
+
+ rnd = ROUND(*xr, 0);
+ if (FLT_EQ(rnd, *xr))
+ *xr = rnd;
+ else
+ *xr = floor(*xr);
+
+ rnd = ROUND(*yr, 0);
+ if (FLT_EQ(rnd, *yr))
+ *yr = rnd;
+ else
+ *yr = floor(*yr);
+
+ RASTER_DEBUGF(4, "Corrected GDALApplyGeoTransform (g -> c) for (%f, %f) = (%f, %f)",
+ xw, yw, *xr, *yr);
+
+ return ES_NONE;
+}
+
+/**
+ * Convert an xw,yw map point to a xr,yr raster point
+ *
+ * @param raster : the raster to get info from
+ * @param xw : X ordinate of the geographical point
+ * @param yw : Y ordinate of the geographical point
+ * @param xr : output parameter, the x ordinate in raster space
+ * @param yr : output parameter, the y ordinate in raster space
+ * @param igt : input/output parameter, inverse geotransform matrix
+ *
+ * @return ES_NONE if success, ES_ERROR if error
+ */
+rt_errorstate
+rt_raster_geopoint_to_rasterpoint(
+ rt_raster raster,
+ double xw, double yw,
+ double *xr, double *yr,
+ double *igt
+) {
+ double _igt[6] = {0};
assert(NULL != raster);
assert(NULL != xr && NULL != yr);
@@ -835,24 +878,10 @@ rt_raster_geopoint_to_cell(
RASTER_DEBUGF(4, "GDALApplyGeoTransform (g -> c) for (%f, %f) = (%f, %f)",
xw, yw, *xr, *yr);
- rnd = ROUND(*xr, 0);
- if (FLT_EQ(rnd, *xr))
- *xr = rnd;
- else
- *xr = floor(*xr);
-
- rnd = ROUND(*yr, 0);
- if (FLT_EQ(rnd, *yr))
- *yr = rnd;
- else
- *yr = floor(*yr);
-
- RASTER_DEBUGF(4, "Corrected GDALApplyGeoTransform (g -> c) for (%f, %f) = (%f, %f)",
- xw, yw, *xr, *yr);
-
return ES_NONE;
}
+
/******************************************************************************
* rt_raster_get_envelope()
******************************************************************************/
@@ -1574,6 +1603,97 @@ rt_raster_clone(rt_raster raster, uint8_t deep) {
}
/******************************************************************************
+* rt_raster_copy_to_geometry()
+******************************************************************************/
+
+rt_errorstate
+rt_raster_copy_to_geometry(
+ rt_raster raster,
+ uint32_t bandnum,
+ char dim,
+ rt_resample_type resample,
+ const LWGEOM *lwgeom_in,
+ LWGEOM **lwgeom_out
+ )
+{
+ int has_z = lwgeom_has_z(lwgeom_in);
+ int has_m = lwgeom_has_m(lwgeom_in);
+ LWGEOM *lwgeom;
+ LWPOINTITERATOR* it;
+ POINT4D p;
+ double igt[6] = {0};
+ rt_errorstate err;
+ rt_band band = NULL;
+ double nodatavalue = 0.0;
+
+ /* Get the band reference and read the nodatavalue */
+ band = rt_raster_get_band(raster, bandnum);
+ if (!band) {
+ rterror("unable to read requested band");
+ return ES_ERROR;
+ }
+ rt_band_get_nodata(band, &nodatavalue);
+
+ /* Fluff up geometry to have space for our new dimension */
+ if (dim == 'z') {
+ if (has_z)
+ lwgeom = lwgeom_clone(lwgeom_in);
+ else if (has_m)
+ lwgeom = lwgeom_force_4d(lwgeom_in, nodatavalue, nodatavalue);
+ else
+ lwgeom = lwgeom_force_3dz(lwgeom_in, nodatavalue);
+ }
+ else if (dim == 'm') {
+ if (has_m)
+ lwgeom = lwgeom_clone(lwgeom_in);
+ if (has_z)
+ lwgeom = lwgeom_force_4d(lwgeom_in, nodatavalue, nodatavalue);
+ else
+ lwgeom = lwgeom_force_3dm(lwgeom_in, nodatavalue);
+ }
+ else {
+ rterror("unknown value for dim");
+ return ES_ERROR;
+ }
+
+ /* Read every point in the geometry */
+ it = lwpointiterator_create_rw(lwgeom);
+ while (lwpointiterator_has_next(it))
+ {
+ int nodata;
+ double xr, yr, value;
+ lwpointiterator_peek(it, &p);
+
+ /* Convert X/Y world coordinates into raster coordinates */
+ err = rt_raster_geopoint_to_rasterpoint(raster, p.x, p.y, &xr, &yr, igt);
+ if (err != ES_NONE) continue;
+
+ /* Read the raster value for this point */
+ err = rt_band_get_pixel_resample(
+ band,
+ xr, yr,
+ resample,
+ &value, &nodata
+ );
+ if (err != ES_NONE) continue;
+
+ /* Copy in the raster value */
+ if (dim == 'z')
+ p.z = value;
+ if (dim == 'm')
+ p.m = value;
+
+ lwpointiterator_modify_next(it, &p);
+ }
+ lwpointiterator_destroy(it);
+
+ if (lwgeom_out)
+ *lwgeom_out = lwgeom;
+ return ES_NONE;
+}
+
+
+/******************************************************************************
* rt_raster_to_gdal()
******************************************************************************/
diff --git a/raster/rt_pg/rtpg_pixel.c b/raster/rt_pg/rtpg_pixel.c
index 6fdae07..f07fab8 100644
--- a/raster/rt_pg/rtpg_pixel.c
+++ b/raster/rt_pg/rtpg_pixel.c
@@ -32,6 +32,8 @@
#include "utils/lsyscache.h" /* for get_typlenbyvalalign */
#include <funcapi.h>
#include "utils/array.h" /* for ArrayType */
+#include "utils/builtins.h" /* for text_to_cstring */
+#include "utils/formatting.h" /* for asc_tolower */
#include "catalog/pg_type.h" /* for INT2OID, INT4OID, FLOAT4OID, FLOAT8OID and TEXTOID */
#include "../../postgis_config.h"
@@ -46,12 +48,14 @@
/* Get pixel value */
Datum RASTER_getPixelValue(PG_FUNCTION_ARGS);
+Datum RASTER_getPixelValueResample(PG_FUNCTION_ARGS);
Datum RASTER_dumpValues(PG_FUNCTION_ARGS);
/* Set pixel value(s) */
Datum RASTER_setPixelValue(PG_FUNCTION_ARGS);
Datum RASTER_setPixelValuesArray(PG_FUNCTION_ARGS);
Datum RASTER_setPixelValuesGeomval(PG_FUNCTION_ARGS);
+Datum RASTER_getGeometryValues(PG_FUNCTION_ARGS);
/* Get pixels of value */
Datum RASTER_pixelOfValue(PG_FUNCTION_ARGS);
@@ -72,69 +76,264 @@ Datum RASTER_neighborhood(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(RASTER_getPixelValue);
Datum RASTER_getPixelValue(PG_FUNCTION_ARGS)
{
- rt_pgraster *pgraster = NULL;
- rt_raster raster = NULL;
- rt_band band = NULL;
- double pixvalue = 0;
- int32_t bandindex = 0;
- int32_t x = 0;
- int32_t y = 0;
- int result = 0;
- bool exclude_nodata_value = TRUE;
+ rt_pgraster *pgraster = NULL;
+ rt_raster raster = NULL;
+ rt_band band = NULL;
+ double pixvalue = 0;
+ int32_t bandindex = 0;
+ int32_t x = 0;
+ int32_t y = 0;
+ int result = 0;
+ bool exclude_nodata_value = TRUE;
int isnodata = 0;
- /* Index is 1-based */
- bandindex = PG_GETARG_INT32(1);
- if ( bandindex < 1 ) {
- elog(NOTICE, "Invalid band index (must use 1-based). Returning NULL");
- PG_RETURN_NULL();
- }
-
- x = PG_GETARG_INT32(2);
-
- y = PG_GETARG_INT32(3);
-
- exclude_nodata_value = PG_GETARG_BOOL(4);
-
- POSTGIS_RT_DEBUGF(3, "Pixel coordinates (%d, %d)", x, y);
-
- /* Deserialize raster */
- if (PG_ARGISNULL(0)) PG_RETURN_NULL();
- pgraster = (rt_pgraster *)PG_DETOAST_DATUM(PG_GETARG_DATUM(0));
-
- raster = rt_raster_deserialize(pgraster, FALSE);
- if (!raster) {
- PG_FREE_IF_COPY(pgraster, 0);
- elog(ERROR, "RASTER_getPixelValue: Could not deserialize raster");
- PG_RETURN_NULL();
- }
-
- /* Fetch Nth band using 0-based internal index */
- band = rt_raster_get_band(raster, bandindex - 1);
- if (! band) {
- elog(NOTICE, "Could not find raster band of index %d when getting pixel "
- "value. Returning NULL", bandindex);
- rt_raster_destroy(raster);
- PG_FREE_IF_COPY(pgraster, 0);
- PG_RETURN_NULL();
- }
- /* Fetch pixel using 0-based coordinates */
- result = rt_band_get_pixel(band, x - 1, y - 1, &pixvalue, &isnodata);
-
- /* If the result is -1 or the value is nodata and we take nodata into account
- * then return nodata = NULL */
- if (result != ES_NONE || (exclude_nodata_value && isnodata)) {
- rt_raster_destroy(raster);
- PG_FREE_IF_COPY(pgraster, 0);
- PG_RETURN_NULL();
- }
-
- rt_raster_destroy(raster);
- PG_FREE_IF_COPY(pgraster, 0);
-
- PG_RETURN_FLOAT8(pixvalue);
+ /* Index is 1-based */
+ bandindex = PG_GETARG_INT32(1);
+ if ( bandindex < 1 ) {
+ elog(NOTICE, "Invalid band index (must use 1-based). Returning NULL");
+ PG_RETURN_NULL();
+ }
+
+ x = PG_GETARG_INT32(2);
+
+ y = PG_GETARG_INT32(3);
+
+ exclude_nodata_value = PG_GETARG_BOOL(4);
+
+ POSTGIS_RT_DEBUGF(3, "Pixel coordinates (%d, %d)", x, y);
+
+ /* Deserialize raster */
+ if (PG_ARGISNULL(0)) PG_RETURN_NULL();
+ pgraster = (rt_pgraster *)PG_DETOAST_DATUM(PG_GETARG_DATUM(0));
+
+ raster = rt_raster_deserialize(pgraster, FALSE);
+ if (!raster) {
+ PG_FREE_IF_COPY(pgraster, 0);
+ elog(ERROR, "RASTER_getPixelValue: Could not deserialize raster");
+ PG_RETURN_NULL();
+ }
+
+ /* Fetch Nth band using 0-based internal index */
+ band = rt_raster_get_band(raster, bandindex - 1);
+ if (! band) {
+ elog(NOTICE, "Could not find raster band of index %d when getting pixel "
+ "value. Returning NULL", bandindex);
+ rt_raster_destroy(raster);
+ PG_FREE_IF_COPY(pgraster, 0);
+ PG_RETURN_NULL();
+ }
+ /* Fetch pixel using 0-based coordinates */
+ result = rt_band_get_pixel(band, x - 1, y - 1, &pixvalue, &isnodata);
+
+ /* If the result is -1 or the value is nodata and we take nodata into account
+ * then return nodata = NULL */
+ if (result != ES_NONE || (exclude_nodata_value && isnodata)) {
+ rt_raster_destroy(raster);
+ PG_FREE_IF_COPY(pgraster, 0);
+ PG_RETURN_NULL();
+ }
+
+ rt_raster_destroy(raster);
+ PG_FREE_IF_COPY(pgraster, 0);
+
+ PG_RETURN_FLOAT8(pixvalue);
+}
+
+static rt_resample_type resample_text_to_type(text *txt)
+{
+ char *resample = asc_tolower(VARDATA(txt), VARSIZE_ANY_EXHDR(txt));
+ if (strncmp(resample, "bilinear", 8) == 0)
+ return RT_BILINEAR;
+ else if (strncmp(resample, "nearest", 7) == 0)
+ return RT_NEAREST;
+ else {
+ elog(ERROR, "Unknown resample type '%s' requested", resample);
+ }
+ pfree(resample);
+ return RT_NEAREST;
+}
+
+/*
+* ST_Value(
+* rast raster,
+* band integer,
+* pt geometry,
+* exclude_nodata_value boolean DEFAULT TRUE,
+* resample text DEFAULT 'nearest'
+*/
+PG_FUNCTION_INFO_V1(RASTER_getPixelValueResample);
+Datum RASTER_getPixelValueResample(PG_FUNCTION_ARGS)
+{
+ rt_raster raster = NULL;
+ rt_band band = NULL;
+ rt_pgraster *pgraster = (rt_pgraster *)PG_DETOAST_DATUM(PG_GETARG_DATUM(0));
+ int32_t bandnum = PG_GETARG_INT32(1);
+ GSERIALIZED *gser;
+ LWPOINT *lwpoint;
+ LWGEOM *lwgeom;
+ bool exclude_nodata_value = PG_GETARG_BOOL(3);
+ rt_resample_type resample_type = RT_NEAREST;
+ double x, y, xr, yr;
+ double pixvalue = 0.0;
+ int isnodata = 0;
+ rt_errorstate err;
+
+ /* Index is 1-based */
+ if (bandnum < 1) {
+ elog(NOTICE, "Invalid band index (must use 1-based). Returning NULL");
+ PG_RETURN_NULL();
+ }
+
+ gser = (GSERIALIZED*)PG_DETOAST_DATUM(PG_GETARG_DATUM(2));
+ if (gserialized_get_type(gser) != POINTTYPE || gserialized_is_empty(gser)) {
+ elog(ERROR, "Attempting to get the value of a pixel with a non-point geometry");
+ PG_RETURN_NULL();
+ }
+
+ raster = rt_raster_deserialize(pgraster, FALSE);
+ if (!raster) {
+ elog(ERROR, "RASTER_getPixelValue: Could not deserialize raster");
+ PG_RETURN_NULL();
+ }
+
+ if (gserialized_get_srid(gser) != rt_raster_get_srid(raster)) {
+ elog(ERROR, "Raster and geometry do not have the same SRID");
+ PG_RETURN_NULL();
+ }
+
+ if (PG_NARGS() > 4) {
+ text *resample_text = PG_GETARG_TEXT_P(4);
+ resample_type = resample_text_to_type(resample_text);
+ }
+
+ /* Fetch Nth band using 0-based internal index */
+ band = rt_raster_get_band(raster, bandnum - 1);
+ if (!band) {
+ elog(ERROR, "Could not find raster band of index %d when getting pixel "
+ "value. Returning NULL", bandnum);
+ PG_RETURN_NULL();
+ }
+
+ /* Get the X/Y coordinates */
+ lwgeom = lwgeom_from_gserialized(gser);
+ lwpoint = lwgeom_as_lwpoint(lwgeom);
+ x = lwpoint_get_x(lwpoint);
+ y = lwpoint_get_y(lwpoint);
+
+ /* Convert X/Y world coordinates into raster coordinates */
+ err = rt_raster_geopoint_to_rasterpoint(raster, x, y, &xr, &yr, NULL);
+ if (err != ES_NONE) {
+ elog(ERROR, "Could not convert world coordinate to raster coordinate");
+ PG_RETURN_NULL();
+ }
+
+ /* Use appropriate resample algorithm */
+ err = rt_band_get_pixel_resample(
+ band,
+ xr, yr,
+ resample_type,
+ &pixvalue, &isnodata
+ );
+
+ /* If the result is -1 or the value is nodata and we take nodata into account
+ * then return nodata = NULL */
+ rt_raster_destroy(raster);
+ lwgeom_free(lwgeom);
+ if (err != ES_NONE || (exclude_nodata_value && isnodata)) {
+ PG_RETURN_NULL();
+ }
+ PG_RETURN_FLOAT8(pixvalue);
+}
+
+
+/*
+* ST_SetZ(
+* rast raster,
+* pt geometry,
+* resample text DEFAULT 'nearest' ),
+* band integer default 1,
+* dimension text default 'z'
+*/
+PG_FUNCTION_INFO_V1(RASTER_getGeometryValues);
+Datum RASTER_getGeometryValues(PG_FUNCTION_ARGS)
+{
+ rt_raster raster = NULL;
+ rt_pgraster *pgraster = NULL;
+ GSERIALIZED *gser;
+ LWGEOM *lwgeom_in, *lwgeom_out;
+ rt_resample_type resample_type = RT_NEAREST;
+ rt_errorstate err;
+ char dimension;
+ const char *func_name;
+ uint16_t num_bands;
+ int32_t band;
+
+ text *resample_text = PG_GETARG_TEXT_P(2);
+
+ /* Dimension depends on the name of calling SQL function */
+ /* ST_SetZ()? or ST_SetM()? */
+ func_name = get_func_name(fcinfo->flinfo->fn_oid);
+ if (strcmp(func_name, "st_setz") == 0)
+ dimension = 'z';
+ else if (strcmp(func_name, "st_setm") == 0)
+ dimension = 'm';
+ else
+ elog(ERROR, "%s called from unexpected SQL signature", __func__);
+
+ /* Geometry */
+ gser = (GSERIALIZED*)PG_DETOAST_DATUM(PG_GETARG_DATUM(1));
+ if (gserialized_is_empty(gser)) {
+ elog(ERROR, "Cannot copy value into an empty geometry");
+ PG_RETURN_NULL();
+ }
+
+ /* Raster */
+ pgraster = (rt_pgraster *)PG_DETOAST_DATUM(PG_GETARG_DATUM(0));
+ raster = rt_raster_deserialize(pgraster, FALSE);
+ num_bands = rt_raster_get_num_bands(raster);
+ if (!raster) {
+ elog(ERROR, "Could not deserialize raster");
+ PG_RETURN_NULL();
+ }
+
+ /* Bandnidex is 1-based */
+ band = PG_GETARG_INT32(3);
+ if (band < 1 || band > num_bands) {
+ elog(NOTICE, "Invalid band index %d. Must be between 1 and %u", band, num_bands);
+ PG_RETURN_NULL();
+ }
+
+ /* SRID consistency */
+ if (gserialized_get_srid(gser) != rt_raster_get_srid(raster)) {
+ elog(ERROR, "Raster and geometry do not have the same SRID");
+ PG_RETURN_NULL();
+ }
+
+ /* Process arguments */
+ resample_type = resample_text_to_type(resample_text);
+
+ /* Get the geometry */
+ lwgeom_in = lwgeom_from_gserialized(gser);
+
+ /* Run the sample */
+ err = rt_raster_copy_to_geometry(
+ raster,
+ band - 1, /* rtcore uses 0-based band number */
+ dimension, /* 'z' or 'm' */
+ resample_type, /* bilinear or nearest */
+ lwgeom_in,
+ &lwgeom_out
+ );
+
+ rt_raster_destroy(raster);
+ lwgeom_free(lwgeom_in);
+ if (err != ES_NONE || !lwgeom_out) {
+ PG_RETURN_NULL();
+ }
+ PG_RETURN_POINTER(gserialized_from_lwgeom(lwgeom_out, NULL));
}
+
/* ---------------------------------------------------------------- */
/* ST_DumpValues function */
/* ---------------------------------------------------------------- */
diff --git a/raster/rt_pg/rtpg_utility.c b/raster/rt_pg/rtpg_utility.c
index 017d265..4e1e920 100644
--- a/raster/rt_pg/rtpg_utility.c
+++ b/raster/rt_pg/rtpg_utility.c
@@ -146,4 +146,3 @@ Datum RASTER_memsize(PG_FUNCTION_ARGS)
PG_RETURN_INT32(size);
}
-
diff --git a/raster/rt_pg/rtpostgis.h b/raster/rt_pg/rtpostgis.h
index 7577d2e..ef9f281 100644
--- a/raster/rt_pg/rtpostgis.h
+++ b/raster/rt_pg/rtpostgis.h
@@ -75,4 +75,5 @@ typedef struct rt_raster_serialized_t rt_pgraster;
#define MAX_DBL_CHARLEN (3 + DBL_MANT_DIG - DBL_MIN_EXP)
#define MAX_INT_CHARLEN 32
+
#endif /* RTPOSTGIS_H_INCLUDED */
diff --git a/raster/rt_pg/rtpostgis.sql.in b/raster/rt_pg/rtpostgis.sql.in
index 95412f6..c31e26c 100644
--- a/raster/rt_pg/rtpostgis.sql.in
+++ b/raster/rt_pg/rtpostgis.sql.in
@@ -4621,39 +4621,26 @@ CREATE OR REPLACE FUNCTION st_value(rast raster, x integer, y integer, exclude_n
AS $$ SELECT st_value($1, 1, $2, $3, $4) $$
LANGUAGE 'sql' IMMUTABLE STRICT PARALLEL SAFE;
-CREATE OR REPLACE FUNCTION st_value(rast raster, band integer, pt geometry, exclude_nodata_value boolean DEFAULT TRUE)
- RETURNS float8 AS
- $$
- DECLARE
- x float8;
- y float8;
- gtype text;
- BEGIN
- gtype := @extschema at .ST_GeometryType(pt);
- IF ( gtype != 'ST_Point' ) THEN
- RAISE EXCEPTION 'Attempting to get the value of a pixel with a non-point geometry';
- END IF;
-
- IF @extschema at .ST_SRID(pt) != @extschema at .ST_SRID(rast) THEN
- RAISE EXCEPTION 'Raster and geometry do not have the same SRID';
- END IF;
-
- x := @extschema at .ST_x(pt);
- y := @extschema at .ST_y(pt);
- RETURN @extschema at .ST_value(rast,
- band,
- @extschema at .ST_worldtorastercoordx(rast, x, y),
- @extschema at .ST_worldtorastercoordy(rast, x, y),
- exclude_nodata_value);
- END;
- $$
- LANGUAGE 'plpgsql' IMMUTABLE STRICT PARALLEL SAFE;
+CREATE OR REPLACE FUNCTION st_value(rast raster, band integer, pt geometry, exclude_nodata_value boolean DEFAULT TRUE, resample text DEFAULT 'nearest')
+ RETURNS float8
+ AS 'MODULE_PATHNAME', 'RASTER_getPixelValueResample'
+ LANGUAGE 'c' IMMUTABLE STRICT PARALLEL SAFE;
-CREATE OR REPLACE FUNCTION ST_Value(rast raster, pt geometry, exclude_nodata_value boolean DEFAULT TRUE)
+CREATE OR REPLACE FUNCTION st_value(rast raster, pt geometry, exclude_nodata_value boolean DEFAULT TRUE)
RETURNS float8
- AS $$ SELECT @extschema at .ST_value($1, 1, $2, $3) $$
+ AS $$ SELECT @extschema at .ST_value($1, 1, $2, $3, 'nearest') $$
LANGUAGE 'sql' IMMUTABLE STRICT PARALLEL SAFE;
+CREATE OR REPLACE FUNCTION st_setz(rast raster, geom geometry, resample text DEFAULT 'nearest', band integer default 1)
+ RETURNS geometry
+ AS 'MODULE_PATHNAME', 'RASTER_getGeometryValues'
+ LANGUAGE 'c' IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OR REPLACE FUNCTION st_setm(rast raster, geom geometry, resample text DEFAULT 'nearest', band integer default 1)
+ RETURNS geometry
+ AS 'MODULE_PATHNAME', 'RASTER_getGeometryValues'
+ LANGUAGE 'c' IMMUTABLE STRICT PARALLEL SAFE;
+
-----------------------------------------------------------------------
-- ST_PixelOfValue()
-----------------------------------------------------------------------
diff --git a/raster/rt_pg/rtpostgis_drop.sql.in b/raster/rt_pg/rtpostgis_drop.sql.in
index 259692c..0fe6986 100644
--- a/raster/rt_pg/rtpostgis_drop.sql.in
+++ b/raster/rt_pg/rtpostgis_drop.sql.in
@@ -120,3 +120,7 @@ DROP FUNCTION IF EXISTS st_approxquantile(rastertable text, rastercolumn text, n
DROP FUNCTION IF EXISTS st_approxquantile(rastertable text, rastercolumn text, sample_percent double precision, quantile double precision);
DROP FUNCTION IF EXISTS st_approxquantile(rastertable text, rastercolumn text, exclude_nodata_value boolean, quantile double precision);
DROP FUNCTION IF EXISTS st_approxquantile(rastertable text, rastercolumn text, quantile double precision);
+
+DROP FUNCTION IF EXISTS st_value(rast raster, band integer, pt geometry, exclude_nodata_value boolean);
+
+
diff --git a/raster/test/cunit/cu_raster_geometry.c b/raster/test/cunit/cu_raster_geometry.c
index 1836441..65a6021 100644
--- a/raster/test/cunit/cu_raster_geometry.c
+++ b/raster/test/cunit/cu_raster_geometry.c
@@ -573,6 +573,69 @@ static void test_raster_pixel_as_polygon() {
cu_free_raster(rast);
}
+
+
+
+static void test_raster_get_pixel_bilinear() {
+ uint32_t width = 2;
+ uint32_t height = 2;
+ double ul_x = 0.0;
+ double ul_y = 0.0;
+ double scale_x = 1;
+ double scale_y = 1;
+
+ double xr, yr;
+ double igt[6];
+
+ rt_raster rast = rt_raster_new(width, height);
+ rt_raster_set_offsets(rast, ul_x, ul_y);
+ rt_raster_set_scale(rast, scale_x, scale_y);
+
+ double xw = 1.5, yw = 0.5;
+
+ rt_raster_generate_new_band(
+ rast, // rt_raster raster,
+ PT_64BF, // rt_pixtype pixtype,
+ 1.0, // double initialvalue,
+ 1, // uint32_t hasnodata,
+ -99.0, // double nodatavalue,
+ 0 // int index
+ );
+
+ rt_raster_geopoint_to_rasterpoint(
+ rast,
+ xw, yw,
+ &xr, &yr, igt);
+
+ printf("xw = %g, yw = %g, xr = %g, yr = %g\n", xw, yw, xr, yr);
+
+ // err = rt_raster_cell_to_geopoint(
+ // rast,
+ // xr, yr,
+ // &xw, &yw, igt);
+
+ // printf("xw = %g, yw = %g, xr = %g, yr = %g\n", xw, yw, xr, yr);
+
+ rt_band band = rt_raster_get_band(rast, 0);
+ rt_band_set_pixel(band, 0, 0, 10.0, NULL);
+ rt_band_set_pixel(band, 0, 1, 10.0, NULL);
+ rt_band_set_pixel(band, 1, 0, 20.0, NULL);
+ rt_band_set_pixel(band, 1, 1, 40.0, NULL);
+
+
+ double value;
+ int nodata;
+ rt_band_get_pixel_bilinear(
+ band,
+ xw, yw, // double xw, double yw,
+ &value, &nodata // double *r_value, int *r_nodata)
+ );
+
+ printf("xw = %g, yw = %g, value = %g, nodata = %d\n", xr, yr, value, nodata);
+
+
+}
+
/* register tests */
void raster_geometry_suite_setup(void);
void raster_geometry_suite_setup(void)
@@ -584,5 +647,6 @@ void raster_geometry_suite_setup(void)
PG_ADD_TEST(suite, test_raster_surface);
PG_ADD_TEST(suite, test_raster_perimeter);
PG_ADD_TEST(suite, test_raster_pixel_as_polygon);
+ PG_ADD_TEST(suite, test_raster_get_pixel_bilinear);
}
diff --git a/raster/test/regress/rt_pixelvalue.sql b/raster/test/regress/rt_pixelvalue.sql
index 4229937..fb9c053 100644
--- a/raster/test/regress/rt_pixelvalue.sql
+++ b/raster/test/regress/rt_pixelvalue.sql
@@ -327,3 +327,32 @@ SELECT 'test 4.4', id
WHERE st_value(st_setvalue(st_setbandnodatavalue(rast, NULL), 1, 1, 1, NULL), 1, 1, 1) != b1val;
DROP TABLE rt_band_properties_test;
+
+-----------------------------------------------------------------------
+-- Test 5 - st_setvalue(rast raster, band integer, geometry, resample)
+-----------------------------------------------------------------------
+
+WITH r AS (
+SELECT
+ST_SetValues(
+ ST_AddBand(
+ ST_MakeEmptyRaster(width => 2, height => 2,
+ upperleftx => 0, upperlefty => 2,
+ scalex => 1.0, scaley => -1.0,
+ skewx => 0, skewy => 0, srid => 4326),
+ index => 1, pixeltype => '16BSI',
+ initialvalue => 0,
+ nodataval => -999),
+ 1,1,1,
+ newvalueset =>ARRAY[ARRAY[10.0::float8, 50.0::float8], ARRAY[40.0::float8, 20.0::float8]]) AS rast
+)
+SELECT
+'Test 5',
+round(ST_Value(rast, 1, 'SRID=4326;POINT(1.5 1.5)'::geometry, resample => 'nearest')) as nearest_15_15,
+round(ST_Value(rast, 1, 'SRID=4326;POINT(0.5 0.5)'::geometry, resample => 'nearest')) as nearest_05_05,
+round(ST_Value(rast, 1, 'SRID=4326;POINT(1.0 1.0)'::geometry, resample => 'bilinear')) as nearest_10_10,
+round(ST_Value(rast, 1, 'SRID=4326;POINT(1.0 0.1)'::geometry, resample => 'bilinear')) as nearest_10_00,
+round(ST_Value(rast, 1, 'SRID=4326;POINT(1.0 1.9)'::geometry, resample => 'bilinear')) as nearest_10_20
+FROM r
+
+
diff --git a/raster/test/regress/rt_pixelvalue_expected b/raster/test/regress/rt_pixelvalue_expected
index 4880f48..49194c7 100644
--- a/raster/test/regress/rt_pixelvalue_expected
+++ b/raster/test/regress/rt_pixelvalue_expected
@@ -8,3 +8,4 @@ NOTICE: Raster do not have a nodata value defined. Set band nodata value first.
NOTICE: Raster do not have a nodata value defined. Set band nodata value first. Nodata value not set. Returning original raster
NOTICE: Raster do not have a nodata value defined. Set band nodata value first. Nodata value not set. Returning original raster
NOTICE: Raster do not have a nodata value defined. Set band nodata value first. Nodata value not set. Returning original raster
+Test 5|50|40|30|26|38
-----------------------------------------------------------------------
Summary of changes:
NEWS | 10 +
doc/reference_raster.xml | 145 +++++++++++++
raster/rt_core/librtcore.h | 87 ++++++++
raster/rt_core/rt_band.c | 156 ++++++++++++++
raster/rt_core/rt_raster.c | 154 ++++++++++++--
raster/rt_pg/rtpg_pixel.c | 317 +++++++++++++++++++++++------
raster/rt_pg/rtpg_utility.c | 1 -
raster/rt_pg/rtpostgis.h | 1 +
raster/rt_pg/rtpostgis.sql.in | 45 ++--
raster/rt_pg/rtpostgis_drop.sql.in | 4 +
raster/test/cunit/cu_raster_geometry.c | 64 ++++++
raster/test/regress/rt_pixelvalue.sql | 29 +++
raster/test/regress/rt_pixelvalue_expected | 1 +
13 files changed, 908 insertions(+), 106 deletions(-)
hooks/post-receive
--
PostGIS
More information about the postgis-tickets
mailing list