mirror of
https://github.com/MariaDB/server.git
synced 2026-05-07 15:45:33 +02:00
Optimized GIS functions
heap/hp_delete.c: Added comments mysql-test/r/gis.result: Updated results after name changes (all results line are unchanged) mysql-test/r/show_check.result: Update test results after fix in hp_delete.cc mysql-test/t/gis.test: Changed table names to longer, hopefully non conflicting ones. Added missing drop table mysys/hash.c: Inendation cleanup mysys/tree.c: Updated comments Decrease tree->allocated on delete (for status) sql/field.cc: Added safety checking for GIS objects sql/gstream.cc: Added copyright message Made a lot of speed/space optimizations Changed class names to be MySQL compliant sql/gstream.h: Made a lot of speed/space optimizations Changed class names to be MySQL compliant sql/item_create.cc: Indentation fixup sql/item_geofunc.cc: Use new gis interface functions and new gis class names. Simple optimizations Indentation fixups Fixed a lot of unlikely but possible errors. sql/item_geofunc.h: Moved SRID_SIZE to spatial.h sql/spatial.cc: Added copyright message Made a lot of speed/space optimizations Changed class names to be MySQL compliant sql/spatial.h: Made a lot of speed/space optimizations Changed class names to be MySQL compliant Indentation fixes Use bool instead of int as result type for functions that only return 0 or 1 sql/sql_string.cc: Simple optimizations sql/sql_string.h: Simple cleanups sql/structs.h: Added LEX_STRING_WITH_INIT (needed by spatial.cc)
This commit is contained in:
parent
f96960f9e1
commit
afa6728a9f
17 changed files with 1599 additions and 1534 deletions
|
|
@ -27,18 +27,17 @@
|
|||
#include "sql_acl.h"
|
||||
#include <m_ctype.h>
|
||||
|
||||
|
||||
String *Item_func_geometry_from_text::val_str(String *str)
|
||||
{
|
||||
Geometry geom;
|
||||
String arg_val;
|
||||
String *wkt= args[0]->val_str(&arg_val);
|
||||
GTextReadStream trs(wkt->ptr(), wkt->length());
|
||||
uint32 srid;
|
||||
Gis_read_stream trs(wkt->c_ptr(), wkt->length());
|
||||
uint32 srid= 0;
|
||||
|
||||
if ((arg_count == 2) && !args[1]->null_value)
|
||||
srid= (uint32)args[1]->val_int();
|
||||
else
|
||||
srid= 0;
|
||||
|
||||
if (str->reserve(SRID_SIZE, 512))
|
||||
return 0;
|
||||
|
|
@ -61,22 +60,19 @@ String *Item_func_geometry_from_wkb::val_str(String *str)
|
|||
String arg_val;
|
||||
String *wkb= args[0]->val_str(&arg_val);
|
||||
Geometry geom;
|
||||
uint32 srid;
|
||||
uint32 srid= 0;
|
||||
|
||||
if ((arg_count == 2) && !args[1]->null_value)
|
||||
srid= (uint32)args[1]->val_int();
|
||||
else
|
||||
srid= 0;
|
||||
|
||||
if (str->reserve(SRID_SIZE, 512))
|
||||
return 0;
|
||||
str->length(0);
|
||||
str->q_append(srid);
|
||||
if ((null_value= (args[0]->null_value ||
|
||||
geom.create_from_wkb(wkb->ptr(), wkb->length()))))
|
||||
geom.create_from_wkb(wkb->ptr(), wkb->length()))) ||
|
||||
str->append(*wkb))
|
||||
return 0;
|
||||
|
||||
str->append(*wkb);
|
||||
return str;
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +88,7 @@ String *Item_func_as_wkt::val_str(String *str)
|
|||
String arg_val;
|
||||
String *swkb= args[0]->val_str(&arg_val);
|
||||
Geometry geom;
|
||||
const char *dummy;
|
||||
|
||||
if ((null_value= (args[0]->null_value ||
|
||||
geom.create_from_wkb(swkb->ptr() + SRID_SIZE,
|
||||
|
|
@ -99,18 +96,19 @@ String *Item_func_as_wkt::val_str(String *str)
|
|||
return 0;
|
||||
|
||||
str->length(0);
|
||||
|
||||
if ((null_value= geom.as_wkt(str)))
|
||||
if ((null_value= geom.as_wkt(str, &dummy)))
|
||||
return 0;
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
void Item_func_as_wkt::fix_length_and_dec()
|
||||
{
|
||||
max_length=MAX_BLOB_WIDTH;
|
||||
}
|
||||
|
||||
|
||||
String *Item_func_as_wkb::val_str(String *str)
|
||||
{
|
||||
String arg_val;
|
||||
|
|
@ -127,11 +125,13 @@ String *Item_func_as_wkb::val_str(String *str)
|
|||
return str;
|
||||
}
|
||||
|
||||
|
||||
void Item_func_as_wkb::fix_length_and_dec()
|
||||
{
|
||||
max_length= MAX_BLOB_WIDTH;
|
||||
}
|
||||
|
||||
|
||||
String *Item_func_geometry_type::val_str(String *str)
|
||||
{
|
||||
String *swkb= args[0]->val_str(str);
|
||||
|
|
@ -141,9 +141,10 @@ String *Item_func_geometry_type::val_str(String *str)
|
|||
geom.create_from_wkb(swkb->ptr() + SRID_SIZE,
|
||||
swkb->length() - SRID_SIZE))))
|
||||
return 0;
|
||||
str->copy(geom.get_class_info()->m_name,
|
||||
strlen(geom.get_class_info()->m_name),
|
||||
default_charset());
|
||||
/* String will not move */
|
||||
str->set(geom.get_class_info()->m_name.str,
|
||||
geom.get_class_info()->m_name.length,
|
||||
default_charset());
|
||||
return str;
|
||||
}
|
||||
|
||||
|
|
@ -153,13 +154,14 @@ String *Item_func_envelope::val_str(String *str)
|
|||
String arg_val;
|
||||
String *swkb= args[0]->val_str(&arg_val);
|
||||
Geometry geom;
|
||||
uint32 srid;
|
||||
|
||||
if ((null_value= args[0]->null_value ||
|
||||
geom.create_from_wkb(swkb->ptr() + SRID_SIZE,
|
||||
swkb->length() - SRID_SIZE)))
|
||||
return 0;
|
||||
|
||||
uint32 srid= uint4korr(swkb->ptr());
|
||||
srid= uint4korr(swkb->ptr());
|
||||
str->length(0);
|
||||
if (str->reserve(SRID_SIZE, 512))
|
||||
return 0;
|
||||
|
|
@ -173,20 +175,21 @@ String *Item_func_centroid::val_str(String *str)
|
|||
String arg_val;
|
||||
String *swkb= args[0]->val_str(&arg_val);
|
||||
Geometry geom;
|
||||
uint32 srid;
|
||||
|
||||
if ((null_value= args[0]->null_value ||
|
||||
geom.create_from_wkb(swkb->ptr() + SRID_SIZE,
|
||||
swkb->length() - SRID_SIZE) ||
|
||||
!GEOM_METHOD_PRESENT(geom, centroid)))
|
||||
geom.create_from_wkb(swkb->ptr() + SRID_SIZE,
|
||||
swkb->length() - SRID_SIZE) ||
|
||||
!GEOM_METHOD_PRESENT(geom, centroid)))
|
||||
return 0;
|
||||
|
||||
if (str->reserve(SRID_SIZE, 512))
|
||||
return 0;
|
||||
str->length(0);
|
||||
uint32 srid= uint4korr(swkb->ptr());
|
||||
srid= uint4korr(swkb->ptr());
|
||||
str->q_append(srid);
|
||||
|
||||
return (null_value= geom.centroid(str)) ? 0 : str;
|
||||
return (null_value= test(geom.centroid(str))) ? 0 : str;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -199,42 +202,42 @@ String *Item_func_spatial_decomp::val_str(String *str)
|
|||
String arg_val;
|
||||
String *swkb= args[0]->val_str(&arg_val);
|
||||
Geometry geom;
|
||||
uint32 srid;
|
||||
|
||||
if ((null_value= (args[0]->null_value ||
|
||||
geom.create_from_wkb(swkb->ptr() + SRID_SIZE,
|
||||
swkb->length() - SRID_SIZE))))
|
||||
return 0;
|
||||
|
||||
null_value= 1;
|
||||
srid= uint4korr(swkb->ptr());
|
||||
if (str->reserve(SRID_SIZE, 512))
|
||||
return 0;
|
||||
goto err;
|
||||
str->length(0);
|
||||
uint32 srid= uint4korr(swkb->ptr());
|
||||
str->q_append(srid);
|
||||
switch(decomp_func)
|
||||
{
|
||||
switch (decomp_func) {
|
||||
case SP_STARTPOINT:
|
||||
if (!GEOM_METHOD_PRESENT(geom,start_point) || geom.start_point(str))
|
||||
goto ret;
|
||||
goto err;
|
||||
break;
|
||||
|
||||
case SP_ENDPOINT:
|
||||
if (!GEOM_METHOD_PRESENT(geom,end_point) || geom.end_point(str))
|
||||
goto ret;
|
||||
goto err;
|
||||
break;
|
||||
|
||||
case SP_EXTERIORRING:
|
||||
if (!GEOM_METHOD_PRESENT(geom,exterior_ring) || geom.exterior_ring(str))
|
||||
goto ret;
|
||||
goto err;
|
||||
break;
|
||||
|
||||
default:
|
||||
goto ret;
|
||||
goto err;
|
||||
}
|
||||
null_value= 0;
|
||||
return str;
|
||||
|
||||
ret:
|
||||
return null_value ? 0 : str;
|
||||
err:
|
||||
null_value= 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -244,43 +247,44 @@ String *Item_func_spatial_decomp_n::val_str(String *str)
|
|||
String *swkb= args[0]->val_str(&arg_val);
|
||||
long n= (long) args[1]->val_int();
|
||||
Geometry geom;
|
||||
uint32 srid;
|
||||
|
||||
if ((null_value= (args[0]->null_value || args[1]->null_value ||
|
||||
geom.create_from_wkb(swkb->ptr() + SRID_SIZE,
|
||||
swkb->length() - SRID_SIZE))))
|
||||
return 0;
|
||||
|
||||
null_value= 1;
|
||||
if (str->reserve(SRID_SIZE, 512))
|
||||
return 0;
|
||||
goto err;
|
||||
srid= uint4korr(swkb->ptr());
|
||||
str->length(0);
|
||||
uint32 srid= uint4korr(swkb->ptr());
|
||||
str->q_append(srid);
|
||||
switch(decomp_func_n)
|
||||
switch (decomp_func_n)
|
||||
{
|
||||
case SP_POINTN:
|
||||
if (!GEOM_METHOD_PRESENT(geom,point_n) || geom.point_n(n,str))
|
||||
goto ret;
|
||||
goto err;
|
||||
break;
|
||||
|
||||
case SP_GEOMETRYN:
|
||||
if (!GEOM_METHOD_PRESENT(geom,geometry_n) || geom.geometry_n(n,str))
|
||||
goto ret;
|
||||
goto err;
|
||||
break;
|
||||
|
||||
case SP_INTERIORRINGN:
|
||||
if (!GEOM_METHOD_PRESENT(geom,interior_ring_n) ||
|
||||
geom.interior_ring_n(n,str))
|
||||
goto ret;
|
||||
goto err;
|
||||
break;
|
||||
|
||||
default:
|
||||
goto ret;
|
||||
goto err;
|
||||
}
|
||||
null_value= 0;
|
||||
return str;
|
||||
|
||||
ret:
|
||||
return null_value ? 0 : str;
|
||||
err:
|
||||
null_value=1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -299,9 +303,9 @@ String *Item_func_point::val_str(String *str)
|
|||
double x= args[0]->val();
|
||||
double y= args[1]->val();
|
||||
|
||||
if ( (null_value= (args[0]->null_value ||
|
||||
args[1]->null_value ||
|
||||
str->realloc(1 + 4 + 8 + 8))))
|
||||
if ((null_value= (args[0]->null_value ||
|
||||
args[1]->null_value ||
|
||||
str->realloc(1 + 4 + SIZEOF_STORED_DOUBLE*2))))
|
||||
return 0;
|
||||
|
||||
str->length(0);
|
||||
|
|
@ -328,11 +332,9 @@ String *Item_func_spatial_collection::val_str(String *str)
|
|||
String arg_value;
|
||||
uint i;
|
||||
|
||||
null_value= 1;
|
||||
|
||||
str->length(0);
|
||||
if (str->reserve(1 + 4 + 4, 512))
|
||||
return 0;
|
||||
goto err;
|
||||
|
||||
str->q_append((char) Geometry::wkbNDR);
|
||||
str->q_append((uint32) coll_type);
|
||||
|
|
@ -342,19 +344,16 @@ String *Item_func_spatial_collection::val_str(String *str)
|
|||
{
|
||||
String *res= args[i]->val_str(&arg_value);
|
||||
if (args[i]->null_value)
|
||||
goto ret;
|
||||
goto err;
|
||||
|
||||
if ( coll_type == Geometry::wkbGeometryCollection )
|
||||
if (coll_type == Geometry::wkbGeometryCollection)
|
||||
{
|
||||
/*
|
||||
In the case of GeometryCollection we don't need
|
||||
any checkings for item types, so just copy them
|
||||
into target collection
|
||||
In the case of GeometryCollection we don't need any checkings
|
||||
for item types, so just copy them into target collection
|
||||
*/
|
||||
if ((null_value= str->reserve(res->length(), 512)))
|
||||
goto ret;
|
||||
|
||||
str->q_append(res->ptr(), res->length());
|
||||
if (str->append(res->ptr(), res->length(), (uint32) 512))
|
||||
goto err;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -363,86 +362,75 @@ String *Item_func_spatial_collection::val_str(String *str)
|
|||
const char *data= res->ptr() + 1;
|
||||
|
||||
/*
|
||||
In the case of named collection we must to
|
||||
check that items are of specific type, let's
|
||||
do this checking now
|
||||
In the case of named collection we must to check that items
|
||||
are of specific type, let's do this checking now
|
||||
*/
|
||||
|
||||
if (len < 5)
|
||||
goto ret;
|
||||
goto err;
|
||||
wkb_type= (Geometry::wkbType) uint4korr(data);
|
||||
data+= 4;
|
||||
len-= 5;
|
||||
if (wkb_type != item_type)
|
||||
goto ret;
|
||||
goto err;
|
||||
|
||||
switch (coll_type) {
|
||||
case Geometry::wkbMultiPoint:
|
||||
case Geometry::wkbMultiLineString:
|
||||
case Geometry::wkbMultiPolygon:
|
||||
if (len < WKB_HEADER_SIZE)
|
||||
goto ret;
|
||||
|
||||
data-= WKB_HEADER_SIZE;
|
||||
len+= WKB_HEADER_SIZE;
|
||||
if (str->reserve(len, 512))
|
||||
goto ret;
|
||||
str->q_append(data, len);
|
||||
if (len < WKB_HEADER_SIZE ||
|
||||
str->append(data-WKB_HEADER_SIZE, len+WKB_HEADER_SIZE, 512))
|
||||
goto err;
|
||||
break;
|
||||
|
||||
case Geometry::wkbLineString:
|
||||
if (str->reserve(POINT_DATA_SIZE, 512))
|
||||
goto ret;
|
||||
str->q_append(data, POINT_DATA_SIZE);
|
||||
if (str->append(data, POINT_DATA_SIZE, 512))
|
||||
goto err;
|
||||
break;
|
||||
|
||||
case Geometry::wkbPolygon:
|
||||
{
|
||||
uint32 n_points;
|
||||
double x1, y1, x2, y2;
|
||||
const char *org_data= data;
|
||||
|
||||
if (len < 4 + 2 * POINT_DATA_SIZE)
|
||||
goto ret;
|
||||
|
||||
uint32 llen= len;
|
||||
const char *ldata= data;
|
||||
goto err;
|
||||
|
||||
n_points= uint4korr(data);
|
||||
data+= 4;
|
||||
float8get(x1, data);
|
||||
data+= 8;
|
||||
data+= SIZEOF_STORED_DOUBLE;
|
||||
float8get(y1, data);
|
||||
data+= 8;
|
||||
data+= SIZEOF_STORED_DOUBLE;
|
||||
|
||||
data+= (n_points - 2) * POINT_DATA_SIZE;
|
||||
|
||||
float8get(x2, data);
|
||||
float8get(y2, data + 8);
|
||||
float8get(y2, data + SIZEOF_STORED_DOUBLE);
|
||||
|
||||
if ((x1 != x2) || (y1 != y2))
|
||||
goto ret;
|
||||
|
||||
if (str->reserve(llen, 512))
|
||||
goto ret;
|
||||
str->q_append(ldata, llen);
|
||||
if ((x1 != x2) || (y1 != y2) ||
|
||||
str->append(org_data, len, 512))
|
||||
goto err;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
goto ret;
|
||||
goto err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (str->length() > current_thd->variables.max_allowed_packet)
|
||||
goto ret;
|
||||
goto err;
|
||||
|
||||
null_value = 0;
|
||||
return str;
|
||||
|
||||
ret:
|
||||
return null_value ? 0 : str;
|
||||
err:
|
||||
null_value= 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Functions for spatial relations
|
||||
*/
|
||||
|
|
@ -453,6 +441,7 @@ longlong Item_func_spatial_rel::val_int()
|
|||
String *res2= args[1]->val_str(&tmp_value2);
|
||||
Geometry g1, g2;
|
||||
MBR mbr1, mbr2;
|
||||
const char *dummy;
|
||||
|
||||
if ((null_value= (args[0]->null_value ||
|
||||
args[1]->null_value ||
|
||||
|
|
@ -460,12 +449,11 @@ longlong Item_func_spatial_rel::val_int()
|
|||
res1->length() - SRID_SIZE) ||
|
||||
g2.create_from_wkb(res2->ptr() + SRID_SIZE,
|
||||
res2->length() - SRID_SIZE) ||
|
||||
g1.get_mbr(&mbr1) ||
|
||||
g2.get_mbr(&mbr2))))
|
||||
g1.get_mbr(&mbr1, &dummy) ||
|
||||
g2.get_mbr(&mbr2, &dummy))))
|
||||
return 0;
|
||||
|
||||
switch (spatial_rel)
|
||||
{
|
||||
switch (spatial_rel) {
|
||||
case SP_CONTAINS_FUNC:
|
||||
return mbr1.contains(&mbr2);
|
||||
case SP_WITHIN_FUNC:
|
||||
|
|
@ -490,6 +478,7 @@ longlong Item_func_spatial_rel::val_int()
|
|||
return 0;
|
||||
}
|
||||
|
||||
|
||||
longlong Item_func_isempty::val_int()
|
||||
{
|
||||
String tmp;
|
||||
|
|
@ -497,23 +486,25 @@ longlong Item_func_isempty::val_int()
|
|||
return args[0]->null_value ? 1 : 0;
|
||||
}
|
||||
|
||||
|
||||
longlong Item_func_issimple::val_int()
|
||||
{
|
||||
String tmp;
|
||||
String *wkb=args[0]->val_str(&tmp);
|
||||
|
||||
if ((null_value= (!wkb || args[0]->null_value )))
|
||||
if ((null_value= (!wkb || args[0]->null_value)))
|
||||
return 0;
|
||||
/* TODO: Ramil or Holyfoot, add real IsSimple calculation */
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
longlong Item_func_isclosed::val_int()
|
||||
{
|
||||
String tmp;
|
||||
String *swkb= args[0]->val_str(&tmp);
|
||||
Geometry geom;
|
||||
int isclosed;
|
||||
int isclosed= 0; // In case of error
|
||||
|
||||
null_value= (!swkb ||
|
||||
args[0]->null_value ||
|
||||
|
|
@ -529,23 +520,26 @@ longlong Item_func_isclosed::val_int()
|
|||
Numerical functions
|
||||
*/
|
||||
|
||||
|
||||
longlong Item_func_dimension::val_int()
|
||||
{
|
||||
uint32 dim;
|
||||
uint32 dim= 0; // In case of error
|
||||
String *swkb= args[0]->val_str(&value);
|
||||
Geometry geom;
|
||||
const char *dummy;
|
||||
|
||||
null_value= (!swkb ||
|
||||
args[0]->null_value ||
|
||||
geom.create_from_wkb(swkb->ptr() + SRID_SIZE,
|
||||
swkb->length() - SRID_SIZE) ||
|
||||
geom.dimension(&dim));
|
||||
geom.dimension(&dim, &dummy));
|
||||
return (longlong) dim;
|
||||
}
|
||||
|
||||
|
||||
longlong Item_func_numinteriorring::val_int()
|
||||
{
|
||||
uint32 num;
|
||||
uint32 num= 0; // In case of error
|
||||
String *swkb= args[0]->val_str(&value);
|
||||
Geometry geom;
|
||||
|
||||
|
|
@ -557,9 +551,10 @@ longlong Item_func_numinteriorring::val_int()
|
|||
return (longlong) num;
|
||||
}
|
||||
|
||||
|
||||
longlong Item_func_numgeometries::val_int()
|
||||
{
|
||||
uint32 num= 0;
|
||||
uint32 num= 0; // In case of errors
|
||||
String *swkb= args[0]->val_str(&value);
|
||||
Geometry geom;
|
||||
|
||||
|
|
@ -571,9 +566,10 @@ longlong Item_func_numgeometries::val_int()
|
|||
return (longlong) num;
|
||||
}
|
||||
|
||||
|
||||
longlong Item_func_numpoints::val_int()
|
||||
{
|
||||
uint32 num;
|
||||
uint32 num= 0; // In case of errors
|
||||
String *swkb= args[0]->val_str(&value);
|
||||
Geometry geom;
|
||||
|
||||
|
|
@ -586,9 +582,10 @@ longlong Item_func_numpoints::val_int()
|
|||
return (longlong) num;
|
||||
}
|
||||
|
||||
|
||||
double Item_func_x::val()
|
||||
{
|
||||
double res;
|
||||
double res= 0.0; // In case of errors
|
||||
String *swkb= args[0]->val_str(&value);
|
||||
Geometry geom;
|
||||
|
||||
|
|
@ -600,9 +597,10 @@ double Item_func_x::val()
|
|||
return res;
|
||||
}
|
||||
|
||||
|
||||
double Item_func_y::val()
|
||||
{
|
||||
double res;
|
||||
double res= 0; // In case of errors
|
||||
String *swkb= args[0]->val_str(&value);
|
||||
Geometry geom;
|
||||
|
||||
|
|
@ -614,23 +612,25 @@ double Item_func_y::val()
|
|||
return res;
|
||||
}
|
||||
|
||||
|
||||
double Item_func_area::val()
|
||||
{
|
||||
double res;
|
||||
double res= 0; // In case of errors
|
||||
String *swkb= args[0]->val_str(&value);
|
||||
Geometry geom;
|
||||
const char *dummy;
|
||||
|
||||
null_value= (!swkb ||
|
||||
geom.create_from_wkb(swkb->ptr() + SRID_SIZE,
|
||||
swkb->length() - SRID_SIZE) ||
|
||||
!GEOM_METHOD_PRESENT(geom, area) ||
|
||||
geom.area(&res));
|
||||
geom.area(&res, &dummy));
|
||||
return res;
|
||||
}
|
||||
|
||||
double Item_func_glength::val()
|
||||
{
|
||||
double res;
|
||||
double res= 0; // In case of errors
|
||||
String *swkb= args[0]->val_str(&value);
|
||||
Geometry geom;
|
||||
|
||||
|
|
@ -650,8 +650,10 @@ longlong Item_func_srid::val_int()
|
|||
null_value= (!swkb ||
|
||||
geom.create_from_wkb(swkb->ptr() + SRID_SIZE,
|
||||
swkb->length() - SRID_SIZE));
|
||||
uint32 res= uint4korr(swkb->ptr());
|
||||
return (longlong) res;
|
||||
if (null_value)
|
||||
return 0;
|
||||
|
||||
return (longlong) (uint4korr(swkb->ptr()));
|
||||
}
|
||||
|
||||
#endif /*HAVE_SPATIAL*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue