Commit 19bd4ed0 authored by Sandro Santilli's avatar Sandro Santilli

Add N-dimensional distance operator with KNN support

Includes docs and tests

git-svn-id: http://svn.osgeo.org/postgis/[email protected] b70326c6-7e19-0410-871a-916f4a2858ee
parent 68c4ad48
......@@ -26,6 +26,8 @@ PostGIS 2.2.0
* 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
......
......@@ -1197,7 +1197,7 @@ Finally the hybrid:
<refname>&lt;#&gt;</refname>
<refpurpose>
Returns the 2D distance between bounding boxes of 2 geometries.
Returns the 2D distance between A and B bounding boxes.
</refpurpose>
</refnamediv>
......@@ -1272,4 +1272,126 @@ SELECT b.tlid, b.mtfcc,
</refsection>
</refentry>
<refentry id="geometry_distance_centroid_nd">
<refnamediv>
<refname>&lt;&lt;-&gt;&gt;</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>&lt;&lt;-&gt;&gt;</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>&lt;&lt;-&gt;&gt;</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>&lt;&lt;#&gt;&gt;</refname>
<refpurpose>
Returns the n-D distance between A and B bounding boxes.
</refpurpose>
</refnamediv>
<refsynopsisdiv>
<funcsynopsis>
<funcprototype>
<funcdef>double precision <function>&lt;&lt;#&gt;&gt;</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>&lt;&lt;#&gt;&gt;</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)') &lt;&lt;#&gt;&gt; geom) instead of g1.geom
&lt;&lt;#&gt;&gt;.
</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>
......@@ -36,6 +36,9 @@
#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
......@@ -75,6 +78,7 @@ Datum gserialized_gist_penalty(PG_FUNCTION_ARGS);
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
......@@ -82,6 +86,8 @@ Datum gserialized_gist_same(PG_FUNCTION_ARGS);
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
......@@ -456,6 +462,98 @@ gserialized_datum_predicate(Datum gs1, Datum gs2, gidx_predicate predicate)
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.
*/
......@@ -482,6 +580,55 @@ gserialized_expand(GSERIALIZED *g, double distance)
* 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.
......@@ -842,8 +989,6 @@ Datum gserialized_gist_union(PG_FUNCTION_ARGS)
}
/*
** GiST support function. Test equality of keys.
*/
......@@ -861,6 +1006,72 @@ Datum gserialized_gist_same(PG_FUNCTION_ARGS)
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);
}
/*
......
......@@ -809,6 +809,39 @@ CREATE OPERATOR &&& (
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
......@@ -817,6 +850,14 @@ CREATE OPERATOR CLASS gist_geometry_ops_nd
-- 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),
......
......@@ -21,10 +21,11 @@ END;
$$;
\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),
......@@ -38,6 +39,67 @@ SELECT '<#> res1',num,
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;
......@@ -2,3 +2,13 @@
<-> 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)
......@@ -141,3 +141,41 @@ WITH v(i,g) AS ( VALUES
)
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
......@@ -57,3 +57,19 @@ ndov6|t
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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment