* New Features *
+ - #3040, KNN GiST index based centroid (<<->>) and box (<<#>>)
+ n-D distance operators (Sandro Santilli / Boundless)
- Interruptibility API for liblwgeom (Sandro Santilli / CartoDB)
- #2939, ST_ClipByBox2D (Sandro Santilli / CartoDB)
- #2247, ST_Retile and ST_CreateOverview: in-db raster overviews creation
<refname><#></refname>
<refpurpose>
-Returns the 2D distance between bounding boxes of 2 geometries.
+Returns the 2D distance between A and B bounding boxes.
</refpurpose>
</refnamediv>
</refsection>
</refentry>
+ <refentry id="geometry_distance_centroid_nd">
+ <refnamediv>
+ <refname><<->></refname>
+
+ <refpurpose>
+Returns the n-D distance between the centroids of A and B bounding
+boxes.
+ </refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <funcsynopsis>
+ <funcprototype>
+ <funcdef>double precision <function><<->></function></funcdef>
+
+ <paramdef>
+ <type>geometry </type>
+
+ <parameter>A</parameter>
+ </paramdef>
+
+ <paramdef>
+ <type>geometry </type>
+
+ <parameter>B</parameter>
+ </paramdef>
+ </funcprototype>
+ </funcsynopsis>
+ </refsynopsisdiv>
+
+ <refsection>
+ <title>Description</title>
+
+ <para>
+The <varname><<->></varname> operator returns the n-D (euclidean)
+distance between the centroids of the bounding boxes of two geometries.
+Useful for doing nearest neighbor
+<emphasis role="strong">approximate</emphasis> distance ordering.
+ </para>
+
+ <note><para>
+This operand will make use of n-D GiST indexes that may be available on
+the geometries. It is different from other operators that use spatial
+indexes in that the spatial index is only used when the operator is in
+the ORDER BY clause.
+ </para></note>
+ <note><para>
+Index only kicks in if one of the geometries is a constant (not in a
+subquery/cte). e.g. 'SRID=3005;POINT(1011102 450541)'::geometry instead
+of a.geom
+ </para></note>
+
+ <para>Availability: 2.2.0 -- KNN only available for PostgreSQL 9.1+</para>
+
+
+ </refsection>
+
+ <refsection>
+ <title>See Also</title>
+ <para>
+<xref linkend="geometry_distance_box_nd" />,
+<xref linkend="geometry_distance_centroid" />
+ </para>
+ </refsection>
+ </refentry>
+
+ <refentry id="geometry_distance_box_nd">
+ <refnamediv>
+ <refname><<#>></refname>
+
+ <refpurpose>
+Returns the n-D distance between A and B bounding boxes.
+ </refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <funcsynopsis>
+ <funcprototype>
+ <funcdef>double precision <function><<#>></function></funcdef>
+
+ <paramdef>
+ <type>geometry </type>
+
+ <parameter>A</parameter>
+ </paramdef>
+
+ <paramdef>
+ <type>geometry </type>
+
+ <parameter>B</parameter>
+ </paramdef>
+ </funcprototype>
+ </funcsynopsis>
+ </refsynopsisdiv>
+
+ <refsection>
+ <title>Description</title>
+
+ <para>The <varname><<#>></varname> operator returns distance between two floating point bounding boxes, possibly reading them from a spatial index (PostgreSQL 9.1+ required). Useful for doing nearest neighbor <emphasis role="strong">approximate</emphasis> distance ordering.</para>
+
+ <note><para>This operand will make use of any indexes that may be available on the
+ geometries. It is different from other operators that use spatial indexes in that the spatial index is only used when the operator
+ is in the ORDER BY clause.</para></note>
+ <note><para>
+Index only kicks in if one of the geometries is a constant e.g. ORDER BY
+(ST_GeomFromText('POINT(1 2)') <<#>> geom) instead of g1.geom
+<<#>>.
+ </para></note>
+
+ <para>Availability: 2.2.0 -- KNN only available for PostgreSQL 9.1+</para>
+
+ </refsection>
+
+ <refsection>
+ <title>See Also</title>
+ <para>
+<xref linkend="geometry_distance_centroid_nd" />,
+<xref linkend="geometry_distance_box" />
+ </para>
+ </refsection>
+ </refentry>
+
</sect1>
#include "gserialized_gist.h" /* For utility functions. */
#include "geography.h"
+#include <assert.h>
+
+
/* Fall back to older finite() if necessary */
#ifndef HAVE_ISFINITE
# ifdef HAVE_GNU_ISFINITE
Datum gserialized_gist_picksplit(PG_FUNCTION_ARGS);
Datum gserialized_gist_union(PG_FUNCTION_ARGS);
Datum gserialized_gist_same(PG_FUNCTION_ARGS);
+Datum gserialized_gist_distance(PG_FUNCTION_ARGS);
/*
** ND Operator prototypes
Datum gserialized_overlaps(PG_FUNCTION_ARGS);
Datum gserialized_contains(PG_FUNCTION_ARGS);
Datum gserialized_within(PG_FUNCTION_ARGS);
+Datum gserialized_distance_box_nd(PG_FUNCTION_ARGS);
+Datum gserialized_distance_centroid_nd(PG_FUNCTION_ARGS);
/*
** GIDX true/false test function type
return LW_FALSE;
}
+/**
+* Calculate the centroid->centroid distance between the boxes.
+*/
+static double gidx_distance_leaf_centroid(const GIDX *a, const GIDX *b)
+{
+ int ndims, i;
+ double sum = 0;
+
+ /* Base computation on least available dimensions */
+ ndims = Min(GIDX_NDIMS(b), GIDX_NDIMS(a));
+ for ( i = 0; i < ndims; ++i )
+ {
+ double ca, cb, d;
+ double amin = GIDX_GET_MIN(a,i);
+ double amax = GIDX_GET_MAX(a,i);
+ double bmin = GIDX_GET_MIN(b,i);
+ double bmax = GIDX_GET_MAX(b,i);
+ ca = amin + ( ( amax - amin ) / 2.0 );
+ cb = bmin + ( ( bmax - bmin ) / 2.0 );
+ d = ca - cb;
+ if ( ! isfinite(d) )
+ {
+ /* Can happen if a dimension was padded with FLT_MAX,
+ * effectively meaning "infinite range". In that case
+ * we take that dimension as adding 0 to the total
+ * distance.
+ */
+ continue;
+ }
+ sum += d * d;
+/*
+ POSTGIS_DEBUGF(3, " centroid of A for dimension %d is %g", i, ca);
+ POSTGIS_DEBUGF(3, " centroid of B for dimension %d is %g", i, cb);
+ POSTGIS_DEBUGF(3, " distance on dimension %d is %g, squared as %g, grows sum to %g", i, d, d*d, sum);
+*/
+ }
+ return sqrt(sum);
+}
+
+/**
+* Calculate the box->box distance.
+*/
+static double gidx_distance(const GIDX *a, const GIDX *b)
+{
+ int ndims, i;
+ double sum = 0;
+
+ /* Base computation on least available dimensions */
+ ndims = Min(GIDX_NDIMS(b), GIDX_NDIMS(a));
+ for ( i = 0; i < ndims; ++i )
+ {
+ double d;
+ double amin = GIDX_GET_MIN(a,i);
+ double amax = GIDX_GET_MAX(a,i);
+ double bmin = GIDX_GET_MIN(b,i);
+ double bmax = GIDX_GET_MAX(b,i);
+ POSTGIS_DEBUGF(3, "A %g - %g", amin, amax);
+ POSTGIS_DEBUGF(3, "B %g - %g", bmin, bmax);
+
+ if ( ( amin <= bmax && amax >= bmin ) )
+ {
+ /* overlaps */
+ d = 0;
+ }
+ else if ( bmax < amin )
+ {
+ /* is "left" */
+ d = amin - bmax;
+ }
+ else
+ {
+ /* is "right" */
+ assert( bmin > amax );
+ d = bmin - amax;
+ }
+ if ( ! isfinite(d) )
+ {
+ /* Can happen if coordinates are corrupted/NaN */
+ continue;
+ }
+ sum += d * d;
+ POSTGIS_DEBUGF(3, "dist %g, squared %g, grows sum to %g", d, d*d, sum);
+ }
+ return sqrt(sum);
+}
+
+static double gidx_distance_node_centroid(const GIDX *node, const GIDX *query)
+{
+ /* TODO: implement ! */
+ return 0;
+}
+
/**
* Return a #GSERIALIZED with an expanded bounding box.
*/
* GiST N-D Index Operator Functions
*/
+PG_FUNCTION_INFO_V1(gserialized_distance_box_nd);
+Datum gserialized_distance_box_nd(PG_FUNCTION_ARGS)
+{
+ char bmem1[GIDX_MAX_SIZE];
+ GIDX *b1 = (GIDX*)bmem1;
+ char bmem2[GIDX_MAX_SIZE];
+ GIDX *b2 = (GIDX*)bmem2;
+ Datum gs1 = PG_GETARG_DATUM(0);
+ Datum gs2 = PG_GETARG_DATUM(1);
+ double distance;
+
+ POSTGIS_DEBUG(3, "entered function");
+
+ /* Must be able to build box for each argument (ie, not empty geometry). */
+ if ( (gserialized_datum_get_gidx_p(gs1, b1) == LW_SUCCESS) &&
+ (gserialized_datum_get_gidx_p(gs2, b2) == LW_SUCCESS) )
+ {
+ distance = gidx_distance(b1, b2);
+ POSTGIS_DEBUGF(3, "got boxes %s and %s", gidx_to_string(b1), gidx_to_string(b2));
+ PG_RETURN_FLOAT8(distance);
+ }
+ PG_RETURN_FLOAT8(FLT_MAX);
+}
+
+
+PG_FUNCTION_INFO_V1(gserialized_distance_centroid_nd);
+Datum gserialized_distance_centroid_nd(PG_FUNCTION_ARGS)
+{
+ char b1mem[GIDX_MAX_SIZE];
+ GIDX *b1 = (GIDX*)b1mem;
+ char b2mem[GIDX_MAX_SIZE];
+ GIDX *b2 = (GIDX*)b2mem;
+ Datum gs1 = PG_GETARG_DATUM(0);
+ Datum gs2 = PG_GETARG_DATUM(1);
+ double distance;
+
+ POSTGIS_DEBUG(3, "entered function");
+
+ /* Must be able to build box for each argument (ie, not empty geometry). */
+ if ( (gserialized_datum_get_gidx_p(gs1, b1) == LW_SUCCESS) &&
+ (gserialized_datum_get_gidx_p(gs2, b2) == LW_SUCCESS) )
+ {
+ distance = gidx_distance_leaf_centroid(b1, b2);
+ POSTGIS_DEBUGF(3, "got boxes %s and %s", gidx_to_string(b1), gidx_to_string(b2));
+ PG_RETURN_FLOAT8(distance);
+ }
+ PG_RETURN_FLOAT8(FLT_MAX);
+}
+
/*
** '~' and operator function. Based on two serialized return true if
** the first is contained by the second.
}
-
-
/*
** GiST support function. Test equality of keys.
*/
PG_RETURN_POINTER(result);
}
+/*
+** GiST support function.
+** Take in a query and an entry and return the "distance" between them.
+**
+** Given an index entry p and a query value q, this function determines the
+** index entry's "distance" from the query value. This function must be
+** supplied if the operator class contains any ordering operators. A query
+** using the ordering operator will be implemented by returning index entries
+** with the smallest "distance" values first, so the results must be consistent
+** with the operator's semantics. For a leaf index entry the result just
+** represents the distance to the index entry; for an internal tree node, the
+** result must be the smallest distance that any child entry could have.
+**
+** Strategy 13 = centroid-based distance tests
+** Strategy 14 = box-based distance tests (not implemented)
+*/
+PG_FUNCTION_INFO_V1(gserialized_gist_distance);
+Datum gserialized_gist_distance(PG_FUNCTION_ARGS)
+{
+ GISTENTRY *entry = (GISTENTRY*) PG_GETARG_POINTER(0);
+ StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
+ char query_box_mem[GIDX_MAX_SIZE];
+ GIDX *query_box = (GIDX*)query_box_mem;
+ GIDX *entry_box;
+ double distance;
+
+ POSTGIS_DEBUG(4, "[GIST] 'distance' function called");
+
+ /* We are using '13' as the gist distance-betweeen-centroids strategy number
+ * and '14' as the gist distance-between-boxes strategy number */
+ if ( strategy != 13 && strategy != 14 ) {
+ elog(ERROR, "unrecognized strategy number: %d", strategy);
+ PG_RETURN_FLOAT8(FLT_MAX);
+ }
+
+ /* Null box should never make this far. */
+ if ( gserialized_datum_get_gidx_p(PG_GETARG_DATUM(1), query_box) == LW_FAILURE )
+ {
+ POSTGIS_DEBUG(4, "[GIST] null query_gbox_index!");
+ PG_RETURN_FLOAT8(FLT_MAX);
+ }
+
+ /* Get the entry box */
+ entry_box = (GIDX*)DatumGetPointer(entry->key);
+
+ /* Box-style distance test */
+ if ( strategy == 14 )
+ {
+ distance = gidx_distance(entry_box, query_box);
+ PG_RETURN_FLOAT8(distance);
+ }
+
+ /* Treat leaf node tests different from internal nodes */
+ if (GIST_LEAF(entry))
+ {
+ /* Calculate distance to leaves */
+ distance = (double)gidx_distance_leaf_centroid(entry_box, query_box);
+ }
+ else
+ {
+ /* Calculate distance for internal nodes */
+ distance = (double)gidx_distance_node_centroid(entry_box, query_box);
+ }
+
+ PG_RETURN_FLOAT8(distance);
+}
/*
JOIN = gserialized_gist_joinsel_nd
);
+-- Availability: 2.2.0
+CREATE OR REPLACE FUNCTION geometry_distance_centroid_nd(geometry,geometry)
+ RETURNS float8
+ AS 'MODULE_PATHNAME', 'gserialized_distance_centroid_nd'
+ LANGUAGE 'c' IMMUTABLE STRICT;
+
+-- Availability: 2.2.0
+CREATE OPERATOR <<->> (
+ LEFTARG = geometry, RIGHTARG = geometry,
+ PROCEDURE = geometry_distance_centroid_nd,
+ COMMUTATOR = '<<->>'
+);
+
+-- Availability: 2.2.0
+CREATE OR REPLACE FUNCTION geometry_distance_box_nd(geom1 geometry, geom2 geometry)
+ RETURNS float8
+ AS 'MODULE_PATHNAME' ,'gserialized_distance_box_nd'
+ LANGUAGE 'c' IMMUTABLE STRICT;
+
+-- Availability: 2.2.0
+CREATE OPERATOR <<#>> (
+ LEFTARG = geometry, RIGHTARG = geometry,
+ PROCEDURE = geometry_distance_box_nd,
+ COMMUTATOR = '<<#>>'
+);
+
+-- Availability: 2.2.0
+CREATE OR REPLACE FUNCTION geometry_gist_distance_nd(internal,geometry,int4)
+ RETURNS float8
+ AS 'MODULE_PATHNAME', 'gserialized_gist_distance'
+ LANGUAGE 'c';
+
+
-- Availability: 2.0.0
CREATE OPERATOR CLASS gist_geometry_ops_nd
FOR TYPE geometry USING GIST AS
-- OPERATOR 6 ~= ,
-- OPERATOR 7 ~ ,
-- OPERATOR 8 @ ,
+#if POSTGIS_PGSQL_VERSION >= 91
+ -- Availability: 2.2.0
+ OPERATOR 13 <<->> FOR ORDER BY pg_catalog.float_ops,
+ -- Availability: 2.2.0
+ OPERATOR 14 <<#>> FOR ORDER BY pg_catalog.float_ops,
+ -- Availability: 2.2.0
+ FUNCTION 8 geometry_gist_distance_nd (internal, geometry, int4),
+#endif
FUNCTION 1 geometry_gist_consistent_nd (internal, geometry, int4),
FUNCTION 2 geometry_gist_union_nd (bytea, internal),
FUNCTION 3 geometry_gist_compress_nd (internal),
$$;
\i regress_lots_of_points.sql
-CREATE INDEX on test using gist (the_geom);
-- Index-supported KNN query
+CREATE INDEX test_gist_2d on test using gist (the_geom);
+
SELECT '<-> idx', qnodes('select * from test order by the_geom <-> ST_MakePoint(0,0) LIMIT 1');
SELECT '<-> res1',num,
(the_geom <-> 'LINESTRING(0 0,5 5)'::geometry)::numeric(10,2),
ST_astext(the_geom) from test
order by the_geom <#> 'LINESTRING(1000 0,1005 5)'::geometry LIMIT 1;
+-- Index-supported nd-KNN query
+
+DROP INDEX test_gist_2d;
+
+UPDATE test set the_geom = ST_MakePoint(
+ ST_X(the_geom), ST_Y(the_geom),
+ num, -num);
+
+SELECT '<<->> seq', qnodes('select * from test order by the_geom <<->> ST_MakePoint(0,0)');
+SELECT '<<#>> seq', qnodes('select * from test order by the_geom <<#>> ST_MakePoint(0,0)');
+
+CREATE INDEX test_gist_nd on test using gist (the_geom gist_geometry_ops_nd);
+
+ANALYZE test;
+
+-- EXT X Y Z M
+-- min 0.0439142361 | 0.0197799355| 1| -50000
+-- max 999.955261 | 999.993652 | 50000| -1
+--SELECT min(st_x(the_geom)) as minx, min(st_y(the_geom)) as miny,
+-- min(st_z(the_geom)) as minz, min(st_m(the_geom)) as minm,
+-- max(st_x(the_geom)) as maxx, max(st_y(the_geom)) as maxy,
+-- max(st_z(the_geom)) as maxz, max(st_m(the_geom)) as maxm
+--FROM test;
+
+
+SELECT '<<->> idx', qnodes('select * from test order by the_geom <<->> ST_MakePoint(0,0) LIMIT 1');
+SELECT '<<->> res1',num,
+ (the_geom <<->> 'LINESTRING(0 0,5 5)'::geometry)::numeric(10,2),
+ ST_astext(the_geom) from test
+ order by the_geom <<->> 'LINESTRING(0 0,5 5)'::geometry LIMIT 1;
+SELECT '<<->> res2',num,
+ (the_geom <<->> 'POINT(95 23 25024 -25025)'::geometry)::numeric(10,2),
+ ST_astext(the_geom) from test
+ order by the_geom <<->> 'POINT(95 23 25024 -25025)'::geometry LIMIT 1;
+SELECT '<<->> res3',num,
+ (the_geom <<->> 'POINT(631 729 25023 -25022)'::geometry)::numeric(10,2),
+ ST_astext(the_geom) from test
+ order by the_geom <<->> 'POINT(631 729 25023 -25022)'::geometry LIMIT 1;
+
+-- EXT X Y Z M
+-- min 0.0439142361 | 0.0197799355| 1| -50000
+-- max 999.955261 | 999.993652 | 50000| -1
+SELECT '<<#>> idx', qnodes('select * from test order by the_geom <<#>> ST_MakePoint(0,0) LIMIT 1');
+SELECT '<<#>> res1',num,
+ (the_geom <<#>> 'LINESTRING(1000 0,1005 5)'::geometry)::numeric(10,2),
+ ST_astext(the_geom) from test
+ order by the_geom <<#>> 'LINESTRING(1000 0,1005 5)'::geometry LIMIT 1;
+-- <<#>> res2|1|2.00|POINT ZM (529.522339 509.260284 1 -1)
+SELECT '<<#>> res2',num,
+ (the_geom <<#>> 'LINESTRING ZM (0 0 -10 -10,1000 1000 -1 -1)'::geometry)::numeric(10,2),
+ ST_astext(the_geom) from test
+ order by the_geom <<#>> 'LINESTRING ZM (0 0 -10 -10,1000 1000 -1 -1)'::geometry LIMIT 1;
+-- <<#>> res3|50000|1.00|POINT ZM (912.12323 831.139587 50000 -50000)
+SELECT '<<#>> res3',num,
+ (the_geom <<#>> 'LINESTRING ZM (0 0 1 -60000,1000 1000 50000 -50001)'::geometry)::numeric(10,2),
+ ST_astext(the_geom) from test
+ order by the_geom <<#>> 'LINESTRING ZM (0 0 1 -60000,1000 1000 50000 -50001)'::geometry LIMIT 1;
+
+
+-- Cleanup
+
DROP FUNCTION qnodes(text);
DROP TABLE test;
<-> res1|48589|0.17|POINT(2.33793712 2.44566727)
<#> idx|Index Scan
<#> res1|2057|0.83|POINT(999.173279 3.92185807)
+<<->> seq|Seq Scan
+<<#>> seq|Seq Scan
+<<->> idx|Index Scan
+<<->> res1|48589|0.17|POINT ZM (2.33793712 2.44566727 48589 -48589)
+<<->> res2|25025|1.20|POINT ZM (95.6546249 23.0995369 25025 -25025)
+<<->> res3|25023|1.27|POINT ZM (631.060242 729.787354 25023 -25023)
+<<#>> idx|Index Scan
+<<#>> res1|2057|0.83|POINT ZM (999.173279 3.92185807 2057 -2057)
+<<#>> res2|1|2.00|POINT ZM (529.522339 509.260284 1 -1)
+<<#>> res3|50000|1.00|POINT ZM (912.12323 831.139587 50000 -50000)
)
SELECT 'ndovm2', array_agg(i) FROM v WHERE g &&& 'POINTZ(0 0 1)'::geometry
ORDER BY 1;
+
+-- nd box centroid distance <<->>
+
+select 'ndcd1', 'LINESTRING(0 0,0 10,10 10)'::geometry <<->>
+ 'LINESTRING(6 2,6 8)'::geometry; -- 1
+select 'ndcd2', 'LINESTRING(0 0,0 10,10 10)'::geometry <<->>
+ 'LINESTRING(11 0,19 10)'::geometry; -- 10
+select 'ndcd3', 'POINTM(0 0 0)'::geometry <<->>
+ 'POINTM(0 0 5)'::geometry; -- 5
+select 'ndcd4', 'POINTZ(0 0 15)'::geometry <<->>
+ 'POINTZ(0 0 10)'::geometry; -- 5
+select 'ndcd5', 'POINTZM(1 2 3 4)'::geometry <<->>
+ 'POINTZM(2 3 4 5)'::geometry; -- 2
+select 'ndcd6', 'POINTZM(9 9 3 4)'::geometry <<->>
+ 'POINT(9 8)'::geometry; -- 1, higher dimensions overlapping
+
+-- nd box distance <<#>>
+
+select 'ndbd1', 'LINESTRING(0 0,0 10,10 10)'::geometry <<#>>
+ 'LINESTRING(6 2,6 8)'::geometry; -- 0, overlap
+select 'ndbd2', 'LINESTRING(0 0,0 10,10 10)'::geometry <<#>>
+ 'LINESTRING(11 0,19 10)'::geometry; -- 1 on the right
+select 'ndbd3', 'LINESTRING(0 0,10 10)'::geometry <<#>>
+ 'LINESTRING(-11 0,-2 10)'::geometry; -- 2 on the left
+select 'ndbd4', 'LINESTRING(0 0,10 10)'::geometry <<#>>
+ 'LINESTRING(0 13,5 14)'::geometry; -- 3 above
+select 'ndbd5', 'LINESTRING(0 0,10 10)'::geometry <<#>>
+ 'LINESTRING(0 -20,5 -4)'::geometry; -- 4 below
+select 'ndbd6', 'LINESTRINGM(0 0 0,1 1 1)'::geometry <<#>>
+ 'LINESTRING(0 0,1 1)'::geometry; -- 0 overlap, mixed
+select 'ndbd7', 'LINESTRINGM(0 0 0,1 1 1)'::geometry <<#>>
+ 'LINESTRINGM(1 1 2,1 1 3)'::geometry; -- 1
+select 'ndbd8', 'LINESTRINGZ(0 0 0,1 1 1)'::geometry <<#>>
+ 'LINESTRINGZ(1 1 3,1 1 5)'::geometry; -- 2
+select 'ndbd9', 'LINESTRINGZ(0 0 0,1 1 1)'::geometry <<#>>
+ 'LINESTRINGM(1 1 3,1 1 5)'::geometry; -- 0, overlap, mixed
+select 'ndbd10', 'LINESTRINGZM(0 0 0 0,1 2 3 4)'::geometry <<#>>
+ 'LINESTRINGZM(3 4 5 6,4 5 6 7)'::geometry; -- 4
ndov7|t
ndovm1|{1,2,3,4,5,8}
ndovm2|{1,2,4,6,7}
+ndcd1|1
+ndcd2|10
+ndcd3|5
+ndcd4|5
+ndcd5|2
+ndcd6|1
+ndbd1|0
+ndbd2|1
+ndbd3|2
+ndbd4|3
+ndbd5|4
+ndbd6|0
+ndbd7|1
+ndbd8|2
+ndbd9|0
+ndbd10|4