]> granicus.if.org Git - postgis/commitdiff
Addition of ST_DumpValues() and regression tests. Ticket #2011
authorBborie Park <bkpark at ucdavis.edu>
Wed, 17 Oct 2012 16:08:16 +0000 (16:08 +0000)
committerBborie Park <bkpark at ucdavis.edu>
Wed, 17 Oct 2012 16:08:16 +0000 (16:08 +0000)
git-svn-id: http://svn.osgeo.org/postgis/trunk@10457 b70326c6-7e19-0410-871a-916f4a2858ee

raster/rt_pg/rt_pg.c
raster/rt_pg/rtpostgis.sql.in.c
raster/test/regress/Makefile.in
raster/test/regress/rt_dumpvalues.sql [new file with mode: 0644]
raster/test/regress/rt_dumpvalues_expected [new file with mode: 0644]

index 2c8c4efde13590021b7b6ee891a4fb5029bc1852..19e5cd219903d941afb6318e64f693208ea25b0f 100644 (file)
@@ -242,6 +242,7 @@ Datum RASTER_setBandNoDataValue(PG_FUNCTION_ARGS);
 
 /* Get pixel value */
 Datum RASTER_getPixelValue(PG_FUNCTION_ARGS);
+Datum RASTER_dumpValues(PG_FUNCTION_ARGS);
 
 /* Set pixel value(s) */
 Datum RASTER_setPixelValue(PG_FUNCTION_ARGS);
@@ -2433,6 +2434,422 @@ Datum RASTER_getPixelValue(PG_FUNCTION_ARGS)
     PG_RETURN_FLOAT8(pixvalue);
 }
 
+/* ---------------------------------------------------------------- */
+/*  ST_DumpValue function                                           */
+/* ---------------------------------------------------------------- */
+
+typedef struct rtpg_dumpvalues_arg_t *rtpg_dumpvalues_arg;
+struct rtpg_dumpvalues_arg_t {
+       int numbands;
+       int rows;
+       int columns;
+
+       int *nbands; /* 0-based */
+       Datum **values;
+       bool **nodata;
+};
+
+static rtpg_dumpvalues_arg rtpg_dumpvalues_arg_init() {
+       rtpg_dumpvalues_arg arg = NULL;
+
+       arg = palloc(sizeof(struct rtpg_dumpvalues_arg_t));
+       if (arg == NULL) {
+               elog(ERROR, "rtpg_dumpvalues_arg_init: Unable to allocate memory for arguments");
+               return NULL;
+       }
+
+       arg->numbands = 0;
+       arg->rows = 0;
+       arg->columns = 0;
+
+       arg->nbands = NULL;
+       arg->values = NULL;
+       arg->nodata = NULL;
+
+       return arg;
+}
+
+static void rtpg_dumpvalues_arg_destroy(rtpg_dumpvalues_arg arg) {
+       int i = 0;
+
+       if (arg->numbands) {
+               if (arg->nbands != NULL)
+                       pfree(arg->nbands);
+
+               for (i = 0; i < arg->numbands; i++) {
+                       if (arg->values[i] != NULL)
+                               pfree(arg->values[i]);
+
+                       if (arg->nodata[i] != NULL)
+                               pfree(arg->nodata[i]);
+               }
+
+               if (arg->values != NULL)
+                       pfree(arg->values);
+               if (arg->nodata != NULL)
+                       pfree(arg->nodata);
+       }
+
+       pfree(arg);
+}
+
+PG_FUNCTION_INFO_V1(RASTER_dumpValues);
+Datum RASTER_dumpValues(PG_FUNCTION_ARGS)
+{
+       FuncCallContext *funcctx;
+       TupleDesc tupdesc;
+       int call_cntr;
+       int max_calls;
+       int i = 0;
+       int x = 0;
+       int y = 0;
+       int z = 0;
+
+       int16 typlen;
+       bool typbyval;
+       char typalign;
+
+       rtpg_dumpvalues_arg arg1 = NULL;
+       rtpg_dumpvalues_arg arg2 = NULL;
+
+       /* stuff done only on the first call of the function */
+       if (SRF_IS_FIRSTCALL()) {
+               MemoryContext oldcontext;
+               rt_pgraster *pgraster = NULL;
+               rt_raster raster = NULL;
+               rt_band band = NULL;
+               int numbands = 0;
+               int j = 0;
+               bool exclude_nodata_value = TRUE;
+
+               ArrayType *array;
+               Oid etype;
+               Datum *e;
+               bool *nulls;
+
+               double val = 0;
+               int hasnodata = 0;
+               double nodataval = 0;
+
+               POSTGIS_RT_DEBUG(2, "RASTER_dumpValues first call");
+
+               /* create a function context for cross-call persistence */
+               funcctx = SRF_FIRSTCALL_INIT();
+
+               /* switch to memory context appropriate for multiple function calls */
+               oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+               /* Get input arguments */
+               if (PG_ARGISNULL(0)) {
+                       MemoryContextSwitchTo(oldcontext);
+                       SRF_RETURN_DONE(funcctx);
+               }
+               pgraster = (rt_pgraster *) PG_DETOAST_DATUM(PG_GETARG_DATUM(0));
+
+               raster = rt_raster_deserialize(pgraster, FALSE);
+               if (!raster) {
+                       PG_FREE_IF_COPY(pgraster, 0);
+                       ereport(ERROR, (
+                               errcode(ERRCODE_OUT_OF_MEMORY),
+                               errmsg("Could not deserialize raster")
+                       ));
+                       MemoryContextSwitchTo(oldcontext);
+                       SRF_RETURN_DONE(funcctx);
+               }
+
+               /* check that raster is not empty */
+               if (rt_raster_is_empty(raster)) {
+                       elog(NOTICE, "Raster provided is empty");
+                       rt_raster_destroy(raster);
+                       PG_FREE_IF_COPY(pgraster, 0);
+                       MemoryContextSwitchTo(oldcontext);
+                       SRF_RETURN_DONE(funcctx);
+               }
+
+               /* raster has bands */
+               numbands = rt_raster_get_num_bands(raster); 
+               if (!numbands) {
+                       elog(NOTICE, "Raster provided has no bands");
+                       rt_raster_destroy(raster);
+                       PG_FREE_IF_COPY(pgraster, 0);
+                       MemoryContextSwitchTo(oldcontext);
+                       SRF_RETURN_DONE(funcctx);
+               }
+
+               /* initialize arg1 */
+               arg1 = rtpg_dumpvalues_arg_init();
+               if (arg1 == NULL) {
+                       elog(ERROR, "RASTER_dumpValues: Unable to initialize argument structure");
+                       rt_raster_destroy(raster);
+                       PG_FREE_IF_COPY(pgraster, 0);
+                       MemoryContextSwitchTo(oldcontext);
+                       SRF_RETURN_DONE(funcctx);
+               }
+
+               /* nband, array */
+               if (!PG_ARGISNULL(1)) {
+                       array = PG_GETARG_ARRAYTYPE_P(1);
+                       etype = ARR_ELEMTYPE(array);
+                       get_typlenbyvalalign(etype, &typlen, &typbyval, &typalign);
+
+                       switch (etype) {
+                               case INT2OID:
+                               case INT4OID:
+                                       break;
+                               default:
+                                       elog(ERROR, "RASTER_dumpValues: Invalid data type for band indexes");
+                                       rtpg_dumpvalues_arg_destroy(arg1);
+                                       rt_raster_destroy(raster);
+                                       PG_FREE_IF_COPY(pgraster, 0);
+                                       MemoryContextSwitchTo(oldcontext);
+                                       SRF_RETURN_DONE(funcctx);
+                                       break;
+                       }
+
+                       deconstruct_array(array, etype, typlen, typbyval, typalign, &e, &nulls, &(arg1->numbands));
+
+                       arg1->nbands = palloc(sizeof(int) * arg1->numbands);
+                       if (arg1->nbands == NULL) {
+                               elog(ERROR, "RASTER_dumpValues: Unable to allocate memory for pixel values");
+                               rtpg_dumpvalues_arg_destroy(arg1);
+                               rt_raster_destroy(raster);
+                               PG_FREE_IF_COPY(pgraster, 0);
+                               MemoryContextSwitchTo(oldcontext);
+                               SRF_RETURN_DONE(funcctx);
+                       }
+
+                       for (i = 0, j = 0; i < arg1->numbands; i++) {
+                               if (nulls[i]) continue;
+
+                               switch (etype) {
+                                       case INT2OID:
+                                               arg1->nbands[j] = DatumGetInt16(e[i]) - 1;
+                                               break;
+                                       case INT4OID:
+                                               arg1->nbands[j] = DatumGetInt32(e[i]) - 1;
+                                               break;
+                               }
+
+                               j++;
+                       }
+
+                       if (j < arg1->numbands) {
+                               arg1->nbands = repalloc(arg1->nbands, sizeof(int) * j);
+                               if (arg1->nbands == NULL) {
+                                       elog(ERROR, "RASTER_dumpValues: Unable to reallocate memory for pixel values");
+                                       rtpg_dumpvalues_arg_destroy(arg1);
+                                       rt_raster_destroy(raster);
+                                       PG_FREE_IF_COPY(pgraster, 0);
+                                       MemoryContextSwitchTo(oldcontext);
+                                       SRF_RETURN_DONE(funcctx);
+                               }
+
+                               arg1->numbands = j;
+                       }
+
+                       /* validate nbands */
+                       for (i = 0; i < arg1->numbands; i++) {
+                               if (!rt_raster_has_band(raster, arg1->nbands[i])) {
+                                       elog(NOTICE, "Band at index %d not found in raster", arg1->nbands[i] + 1);
+                                       rtpg_dumpvalues_arg_destroy(arg1);
+                                       rt_raster_destroy(raster);
+                                       PG_FREE_IF_COPY(pgraster, 0);
+                                       MemoryContextSwitchTo(oldcontext);
+                                       SRF_RETURN_DONE(funcctx);
+                               }
+                       }
+
+               }
+               else {
+                       arg1->numbands = numbands;
+                       arg1->nbands = palloc(sizeof(int) * arg1->numbands);
+
+                       if (arg1->nbands == NULL) {
+                               elog(ERROR, "RASTER_dumpValues: Unable to allocate memory for pixel values");
+                               rtpg_dumpvalues_arg_destroy(arg1);
+                               rt_raster_destroy(raster);
+                               PG_FREE_IF_COPY(pgraster, 0);
+                               MemoryContextSwitchTo(oldcontext);
+                               SRF_RETURN_DONE(funcctx);
+                       }
+
+                       for (i = 0; i < arg1->numbands; i++) {
+                               arg1->nbands[i] = i;
+                               POSTGIS_RT_DEBUGF(4, "arg1->nbands[%d] = %d", arg1->nbands[i], i);
+                       }
+               }
+
+               arg1->rows = rt_raster_get_height(raster);
+               arg1->columns = rt_raster_get_width(raster);
+
+               /* exclude_nodata_value */
+               if (!PG_ARGISNULL(2))
+                       exclude_nodata_value = PG_GETARG_BOOL(2);
+               POSTGIS_RT_DEBUGF(4, "exclude_nodata_value = %d", exclude_nodata_value);
+
+               /* allocate memory for each band's values and nodata flags */
+               arg1->values = palloc(sizeof(Datum *) * arg1->numbands);
+               arg1->nodata = palloc(sizeof(bool *) * arg1->numbands);
+               if (arg1->values == NULL || arg1->nodata == NULL) {
+                       elog(ERROR, "RASTER_dumpValues: Unable to allocate memory for pixel values");
+                       rtpg_dumpvalues_arg_destroy(arg1);
+                       rt_raster_destroy(raster);
+                       PG_FREE_IF_COPY(pgraster, 0);
+                       MemoryContextSwitchTo(oldcontext);
+                       SRF_RETURN_DONE(funcctx);
+               }
+               memset(arg1->values, 0, sizeof(Datum *) * arg1->numbands);
+               memset(arg1->nodata, 0, sizeof(bool *) * arg1->numbands);
+
+               /* get each band and dump data */
+               for (z = 0; z < arg1->numbands; z++) {
+                       band = rt_raster_get_band(raster, arg1->nbands[z]);
+                       if (!band) {
+                               elog(ERROR, "RASTER_dumpValues: Unable to get band at index %d", arg1->nbands[z] + 1);
+                               rtpg_dumpvalues_arg_destroy(arg1);
+                               rt_raster_destroy(raster);
+                               PG_FREE_IF_COPY(pgraster, 0);
+                               MemoryContextSwitchTo(oldcontext);
+                               SRF_RETURN_DONE(funcctx);
+                       }
+
+                       /* band's hasnodata and nodataval */
+                       hasnodata = rt_band_get_hasnodata_flag(band);
+                       if (hasnodata)
+                               nodataval = rt_band_get_nodata(band);
+                       POSTGIS_RT_DEBUGF(4, "(hasnodata, nodataval) = (%d, %f)", hasnodata, nodataval);
+
+                       /* allocate memory for values and nodata flags */
+                       arg1->values[z] = palloc(sizeof(Datum) * arg1->rows * arg1->columns);
+                       arg1->nodata[z] = palloc(sizeof(bool) * arg1->rows * arg1->columns);
+                       if (arg1->values[z] == NULL || arg1->nodata[z] == NULL) {
+                               elog(ERROR, "RASTER_dumpValues: Unable to allocate memory for pixel values");
+                               rtpg_dumpvalues_arg_destroy(arg1);
+                               rt_raster_destroy(raster);
+                               PG_FREE_IF_COPY(pgraster, 0);
+                               MemoryContextSwitchTo(oldcontext);
+                               SRF_RETURN_DONE(funcctx);
+                       }
+                       memset(arg1->values[z], 0, sizeof(Datum) * arg1->rows * arg1->columns);
+                       memset(arg1->nodata[z], 0, sizeof(bool) * arg1->rows * arg1->columns);
+
+                       i = 0;
+                       for (y = 0; y < arg1->rows; y++) {
+                               for (x = 0; x < arg1->columns; x++) {
+                                       /* get pixel */
+                                       if (rt_band_get_pixel(band, x, y, &val) != 0) {
+                                               elog(ERROR, "RASTER_dumpValues: Unable to pixel (%d, %d) of band %d", x, y, arg1->nbands[z] + 1);
+                                               rtpg_dumpvalues_arg_destroy(arg1);
+                                               rt_raster_destroy(raster);
+                                               PG_FREE_IF_COPY(pgraster, 0);
+                                               MemoryContextSwitchTo(oldcontext);
+                                               SRF_RETURN_DONE(funcctx);
+                                       }
+
+                                       arg1->values[z][i] = Float8GetDatum(val);
+                                       POSTGIS_RT_DEBUGF(5, "arg1->values[z][i] = %f", DatumGetFloat8(arg1->values[z][i]));
+                                       if (hasnodata) {
+                                               POSTGIS_RT_DEBUGF(5, "FLT_EQ?: %d", FLT_EQ(val, nodataval) ? 1 : 0);
+                                       }
+                                       POSTGIS_RT_DEBUGF(5, "clamped is?: %d", rt_band_clamped_value_is_nodata(band, val));
+
+                                       if (
+                                               exclude_nodata_value &&
+                                               hasnodata && (
+                                                       FLT_EQ(val, nodataval) ||
+                                                       rt_band_clamped_value_is_nodata(band, val) == 1
+                                               )
+                                       ) {
+                                               arg1->nodata[z][i] = TRUE;
+                                               POSTGIS_RT_DEBUG(5, "nodata = 1");
+                                       }
+                                       else
+                                               POSTGIS_RT_DEBUG(5, "nodata = 0");
+
+                                       i++;
+                               }
+                       }
+               }
+
+               /* cleanup */
+               rt_raster_destroy(raster);
+               PG_FREE_IF_COPY(pgraster, 0);
+
+               /* Store needed information */
+               funcctx->user_fctx = arg1;
+
+               /* total number of tuples to be returned */
+               funcctx->max_calls = arg1->numbands;
+
+               /* Build a tuple descriptor for our result type */
+               if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) {
+                       MemoryContextSwitchTo(oldcontext);
+                       ereport(ERROR, (
+                               errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                               errmsg(
+                                       "function returning record called in context "
+                                       "that cannot accept type record"
+                               )
+                       ));
+               }
+
+               BlessTupleDesc(tupdesc);
+               funcctx->tuple_desc = tupdesc;
+
+               MemoryContextSwitchTo(oldcontext);
+       }
+
+       /* stuff done on every call of the function */
+       funcctx = SRF_PERCALL_SETUP();
+
+       call_cntr = funcctx->call_cntr;
+       max_calls = funcctx->max_calls;
+       tupdesc = funcctx->tuple_desc;
+       arg2 = funcctx->user_fctx;
+
+       /* do when there is more left to send */
+       if (call_cntr < max_calls) {
+               int values_length = 2;
+               Datum values[values_length];
+               bool nulls[values_length];
+               HeapTuple tuple;
+               Datum result;
+               ArrayType *mdValues = NULL;
+               int dim[2] = {arg2->rows, arg2->columns};
+               int lbound[2] = {1, 1};
+
+               POSTGIS_RT_DEBUGF(3, "call number %d", call_cntr);
+               POSTGIS_RT_DEBUGF(4, "dim = %d, %d", dim[0], dim[1]);
+
+               memset(nulls, FALSE, sizeof(bool) * values_length);
+
+               values[0] = Int32GetDatum(arg2->nbands[call_cntr] + 1);
+
+               /* info about the type of item in the multi-dimensional array (float8). */
+               get_typlenbyvalalign(FLOAT8OID, &typlen, &typbyval, &typalign);
+
+               /* assemble 3-dimension array of values */
+               mdValues = construct_md_array(
+                       arg2->values[call_cntr], arg2->nodata[call_cntr],
+                       2, dim, lbound,
+                       FLOAT8OID,
+                       typlen, typbyval, typalign
+               );
+               values[1] = PointerGetDatum(mdValues);
+
+               /* build a tuple and datum */
+               tuple = heap_form_tuple(tupdesc, values, nulls);
+               result = HeapTupleGetDatum(tuple);
+
+               SRF_RETURN_NEXT(funcctx, result);
+       }
+       /* do when there is no more left */
+       else {
+               rtpg_dumpvalues_arg_destroy(arg2);
+               SRF_RETURN_DONE(funcctx);
+       }
+}
+
 /**
  * Write value of raster sample on given position and in specified band.
  */
index 23fe8b188b7693dd01c8e14566a2d58b1b37d6b1..aaf49a06ea9b56deb8efaf3cdd156e9ab118cd93 100644 (file)
@@ -4119,6 +4119,22 @@ CREATE OR REPLACE FUNCTION st_dumpaspolygons(rast raster, band integer DEFAULT 1
        AS 'MODULE_PATHNAME','RASTER_dumpAsPolygons'
        LANGUAGE 'c' IMMUTABLE STRICT;
 
+-----------------------------------------------------------------------
+-- ST_DumpValues
+-----------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION st_dumpvalues(
+       rast raster, nband integer[] DEFAULT NULL, exclude_nodata_value boolean DEFAULT TRUE,
+       OUT nband integer, OUT valarray double precision[][]
+)
+       RETURNS SETOF record
+       AS 'MODULE_PATHNAME','RASTER_dumpValues'
+       LANGUAGE 'c' IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION st_dumpvalues(rast raster, nband integer, exclude_nodata_value boolean DEFAULT TRUE)
+       RETURNS double precision[][]
+       AS $$ SELECT valarray FROM st_dumpvalues($1, ARRAY[$2]::integer[], $3) $$
+       LANGUAGE 'sql' IMMUTABLE STRICT;
+
 -----------------------------------------------------------------------
 -- ST_Polygon
 -----------------------------------------------------------------------
index c41deb05a256e7b0104e97842e60876c281f0282..d507b46d1cede30daae0dc3d204ae2be22bd059d 100644 (file)
@@ -88,6 +88,7 @@ TEST_UTILITY = \
        rt_reclass \
        rt_resample \
        rt_asraster \
+       rt_dumpvalues
 
 TEST_MAPALGEBRA = \
        rt_mapalgebraexpr \
diff --git a/raster/test/regress/rt_dumpvalues.sql b/raster/test/regress/rt_dumpvalues.sql
new file mode 100644 (file)
index 0000000..a754f8e
--- /dev/null
@@ -0,0 +1,99 @@
+SET client_min_messages TO warning;
+DROP TABLE IF EXISTS raster_dumpvalues;
+CREATE TABLE raster_dumpvalues (
+       rid integer,
+       rast raster
+);
+CREATE OR REPLACE FUNCTION make_raster(
+       rast raster DEFAULT NULL,
+       pixtype text DEFAULT '8BUI',
+       rows integer DEFAULT 3,
+       columns integer DEFAULT 3,
+       nodataval double precision DEFAULT 0,
+       start_val double precision DEFAULT 1,
+       step double precision DEFAULT 1,
+       skip_expr text DEFAULT NULL
+)
+       RETURNS raster
+       AS $$
+       DECLARE
+               x int;
+               y int;
+               value double precision;
+               values double precision[][][];
+               result boolean;
+               expr text;
+               _rast raster;
+               nband int;
+       BEGIN
+               IF rast IS NULL THEN
+                       nband := 1;
+                       _rast := ST_AddBand(ST_MakeEmptyRaster(columns, rows, 0, 0, 1, -1, 0, 0, 0), nband, pixtype, 0, nodataval);
+               ELSE
+                       nband := ST_NumBands(rast) + 1;
+                       _rast := ST_AddBand(rast, nband, pixtype, 0, nodataval);
+               END IF;
+
+               value := start_val;
+               values := array_fill(NULL::double precision, ARRAY[columns, rows]);
+
+               FOR y IN 1..columns LOOP
+                       FOR x IN 1..rows LOOP
+                               IF skip_expr IS NULL OR length(skip_expr) < 1 THEN
+                                       result := TRUE;
+                               ELSE
+                                       expr := replace(skip_expr, '[v]'::text, value::text);
+                                       EXECUTE 'SELECT (' || expr || ')::boolean' INTO result;
+                               END IF;
+                               
+                               IF result IS TRUE THEN
+                                       values[y][x] := value;
+                               END IF;
+
+                               value := value + step;
+                       END LOOP;
+               END LOOP;
+
+               _rast := ST_SetValues(_rast, nband, 1, 1, values);
+               RETURN _rast;
+       END;
+       $$ LANGUAGE 'plpgsql';
+
+INSERT INTO raster_dumpvalues
+       SELECT 1, make_raster(NULL, '8BSI', 3, 3, 0, 1) UNION ALL
+       SELECT 2, make_raster(NULL, '8BSI', 3, 3, 0, -1) UNION ALL
+       SELECT 3, make_raster(NULL, '8BSI', 3, 3, 0, 1) UNION ALL
+       SELECT 4, make_raster(NULL, '8BSI', 3, 3, 0, -2) UNION ALL
+       SELECT 5, make_raster(NULL, '8BSI', 3, 3, 0, 2)
+;
+
+INSERT INTO raster_dumpvalues
+       SELECT
+               rid + 10,
+               make_raster(rast, '16BSI', 3, 3, rid, (rid / 2)::integer)
+       FROM raster_dumpvalues
+       WHERE rid <= 10;
+
+INSERT INTO raster_dumpvalues
+       SELECT
+               rid + 10,
+               make_raster(rast, '32BSI', 3, 3, rid, (rid / 2)::integer)
+       FROM raster_dumpvalues
+       WHERE rid BETWEEN 11 AND 20;
+
+DROP FUNCTION IF EXISTS make_raster(raster, text, integer, integer, double precision, double precision, double precision, text);
+
+SELECT
+       rid,
+       (ST_DumpValues(rast)).*
+FROM raster_dumpvalues
+ORDER BY rid;
+
+SELECT
+       rid,
+       (ST_DumpValues(rast, ARRAY[3,2,1])).*
+FROM raster_dumpvalues
+WHERE rid > 20
+ORDER BY rid;
+
+DROP TABLE IF EXISTS raster_dumpvalues;
diff --git a/raster/test/regress/rt_dumpvalues_expected b/raster/test/regress/rt_dumpvalues_expected
new file mode 100644 (file)
index 0000000..b31b5be
--- /dev/null
@@ -0,0 +1,45 @@
+1|1|{{1,2,3},{4,5,6},{7,8,9}}
+2|1|{{-1,NULL,1},{2,3,4},{5,6,7}}
+3|1|{{1,2,3},{4,5,6},{7,8,9}}
+4|1|{{-2,-1,NULL},{1,2,3},{4,5,6}}
+5|1|{{2,3,4},{5,6,7},{8,9,10}}
+11|1|{{1,2,3},{4,5,6},{7,8,9}}
+11|2|{{0,NULL,2},{3,4,5},{6,7,8}}
+12|1|{{-1,NULL,1},{2,3,4},{5,6,7}}
+12|2|{{1,NULL,3},{4,5,6},{7,8,9}}
+13|1|{{1,2,3},{4,5,6},{7,8,9}}
+13|2|{{1,2,NULL},{4,5,6},{7,8,9}}
+14|1|{{-2,-1,NULL},{1,2,3},{4,5,6}}
+14|2|{{2,3,NULL},{5,6,7},{8,9,10}}
+15|1|{{2,3,4},{5,6,7},{8,9,10}}
+15|2|{{2,3,4},{NULL,6,7},{8,9,10}}
+21|1|{{1,2,3},{4,5,6},{7,8,9}}
+21|2|{{0,NULL,2},{3,4,5},{6,7,8}}
+21|3|{{5,6,7},{8,9,10},{NULL,12,13}}
+22|1|{{-1,NULL,1},{2,3,4},{5,6,7}}
+22|2|{{1,NULL,3},{4,5,6},{7,8,9}}
+22|3|{{6,7,8},{9,10,11},{NULL,13,14}}
+23|1|{{1,2,3},{4,5,6},{7,8,9}}
+23|2|{{1,2,NULL},{4,5,6},{7,8,9}}
+23|3|{{6,7,8},{9,10,11},{12,NULL,14}}
+24|1|{{-2,-1,NULL},{1,2,3},{4,5,6}}
+24|2|{{2,3,NULL},{5,6,7},{8,9,10}}
+24|3|{{7,8,9},{10,11,12},{13,NULL,15}}
+25|1|{{2,3,4},{5,6,7},{8,9,10}}
+25|2|{{2,3,4},{NULL,6,7},{8,9,10}}
+25|3|{{7,8,9},{10,11,12},{13,14,NULL}}
+21|3|{{5,6,7},{8,9,10},{NULL,12,13}}
+21|2|{{0,NULL,2},{3,4,5},{6,7,8}}
+21|1|{{1,2,3},{4,5,6},{7,8,9}}
+22|3|{{6,7,8},{9,10,11},{NULL,13,14}}
+22|2|{{1,NULL,3},{4,5,6},{7,8,9}}
+22|1|{{-1,NULL,1},{2,3,4},{5,6,7}}
+23|3|{{6,7,8},{9,10,11},{12,NULL,14}}
+23|2|{{1,2,NULL},{4,5,6},{7,8,9}}
+23|1|{{1,2,3},{4,5,6},{7,8,9}}
+24|3|{{7,8,9},{10,11,12},{13,NULL,15}}
+24|2|{{2,3,NULL},{5,6,7},{8,9,10}}
+24|1|{{-2,-1,NULL},{1,2,3},{4,5,6}}
+25|3|{{7,8,9},{10,11,12},{13,14,NULL}}
+25|2|{{2,3,4},{NULL,6,7},{8,9,10}}
+25|1|{{2,3,4},{5,6,7},{8,9,10}}