From: Sandro Santilli Date: Tue, 5 Oct 2004 07:53:02 +0000 (+0000) Subject: ZM aware WKT/WKB input/output. X-Git-Tag: pgis_1_0_0RC1~348 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=c4346045543b974f32714ed98021f5fa42fdecb2;p=postgis ZM aware WKT/WKB input/output. git-svn-id: http://svn.osgeo.org/postgis/trunk@924 b70326c6-7e19-0410-871a-916f4a2858ee --- diff --git a/lwgeom/liblwgeom.h b/lwgeom/liblwgeom.h index 9238f4cb2..a689e6a44 100644 --- a/lwgeom/liblwgeom.h +++ b/lwgeom/liblwgeom.h @@ -291,7 +291,7 @@ extern int pointArray_ptsize(const POINTARRAY *pa); * * LWGEOM types are an 8-bit char in this format: * - * BSDDtttt + * BSZMtttt * * WHERE * B = 16 byte BOX2DFLOAT4 follows (probably not aligned) [before SRID] diff --git a/lwgeom/lwgparse.c b/lwgeom/lwgparse.c index 7382948e4..b63cfb397 100644 --- a/lwgeom/lwgparse.c +++ b/lwgeom/lwgparse.c @@ -72,6 +72,8 @@ struct { int flags; int srid; int ndims; + int hasZ; + int hasM; /* create integer version */ int lwgi; /* input is integer (wkb only)*/ @@ -149,7 +151,9 @@ byte* parse_it(const char* geometry,allocator allocfunc,report_error errfunc); byte* parse_lwg(const char* geometry,allocator allocfunc,report_error errfunc); byte* parse_lwgi(const char* geometry,allocator allocfunc,report_error errfunc); -void set_srid(double d_srid){ +void +set_srid(double d_srid) +{ if ( d_srid >= 0 ) d_srid+=0.1; else @@ -159,7 +163,9 @@ void set_srid(double d_srid){ srid=(int)(d_srid+0.1); } -tuple* alloc_tuple(output_func of,size_t size){ +tuple * +alloc_tuple(output_func of,size_t size) +{ tuple* ret = free_list; if ( ! ret ){ @@ -195,12 +201,16 @@ tuple* alloc_tuple(output_func of,size_t size){ return ret; } -static void error(const char* err){ +static void +error(const char* err) +{ error_func(err); ferror_occured=1; } -void free_tuple(tuple* to_free){ +void +free_tuple(tuple* to_free) +{ tuple* list_end = to_free; @@ -215,14 +225,18 @@ void free_tuple(tuple* to_free){ free_list = to_free; } -void inc_num(void){ +void +inc_num(void) +{ the_geom.stack->num++; } /* Allocate a 'counting' tuple */ -void alloc_stack_tuple(int type,output_func of,size_t size){ +void +alloc_stack_tuple(int type,output_func of,size_t size) +{ tuple* p; inc_num(); @@ -234,11 +248,15 @@ void alloc_stack_tuple(int type,output_func of,size_t size){ the_geom.stack = p; } -void pop(void){ +void +pop(void) +{ the_geom.stack = the_geom.stack->stack_next; } -void popc(void){ +void +popc(void) +{ if ( the_geom.stack->num < minpoints){ error("geometry requires more points"); } @@ -246,14 +264,16 @@ void popc(void){ } -void check_dims(int num){ - +void +check_dims(int num) +{ if( the_geom.ndims != num){ if (the_geom.ndims) { error("Can not mix dimentionality in a geometry"); - } - else{ + } else { the_geom.ndims = num; + if ( num > 2 ) the_geom.hasZ = 1; + if ( num > 3 ) the_geom.hasM = 1; } } } @@ -269,7 +289,9 @@ void check_dims(int num){ machine */ #ifdef SHRINK_INTS -void WRITE_INT4(output_state * out,int4 val){ +void +WRITE_INT4(output_state * out,int4 val) +{ if ( val <= 0x7f ){ if ( getMachineEndian() == LITTLE_ENDIAN ){ val = (val<<1) | 1; @@ -293,7 +315,9 @@ void WRITE_INT4(output_state * out,int4 val){ #endif -void WRITE_DOUBLES(output_state* out,double* points, int cnt){ +void +WRITE_DOUBLES(output_state* out,double* points, int cnt) +{ if ( the_geom.lwgi){ int4 vals[4]; int i; @@ -311,15 +335,21 @@ void WRITE_DOUBLES(output_state* out,double* points, int cnt){ } -void write_size(tuple* this,output_state* out){ +void +write_size(tuple* this,output_state* out) +{ WRITE_INT4_REAL(out,the_geom.alloc_size); } -void alloc_lwgeom(int srid){ +void +alloc_lwgeom(int srid) +{ the_geom.srid=srid; the_geom.alloc_size=0; the_geom.stack=NULL; the_geom.ndims=0; + the_geom.hasZ=0; + the_geom.hasM=0; //Free if used already if ( the_geom.first ){ @@ -334,30 +364,45 @@ void alloc_lwgeom(int srid){ the_geom.stack = alloc_tuple(write_size,4); } - -void write_point_2(tuple* this,output_state* out){ +void +write_point_2(tuple* this,output_state* out) +{ WRITE_DOUBLES(out,this->points,2); } -void write_point_3(tuple* this,output_state* out){ +void +write_point_3(tuple* this,output_state* out) +{ WRITE_DOUBLES(out,this->points,3); } -void write_point_4(tuple* this,output_state* out){ + +void +write_point_4(tuple* this,output_state* out) +{ WRITE_DOUBLES(out,this->points,4); } -void write_point_2i(tuple* this,output_state* out){ +void +write_point_2i(tuple* this,output_state* out) +{ WRITE_INT4_REAL_MULTIPLE(out,this->points,2); } -void write_point_3i(tuple* this,output_state* out){ +void +write_point_3i(tuple* this,output_state* out) +{ WRITE_INT4_REAL_MULTIPLE(out,this->points,3); } -void write_point_4i(tuple* this,output_state* out){ + +void +write_point_4i(tuple* this,output_state* out) +{ WRITE_INT4_REAL_MULTIPLE(out,this->points,4); } -void alloc_point_2d(double x,double y){ +void +alloc_point_2d(double x,double y) +{ tuple* p = alloc_tuple(write_point_2,the_geom.lwgi?8:16); p->points[0] = x; p->points[1] = y; @@ -365,7 +410,9 @@ void alloc_point_2d(double x,double y){ check_dims(2); } -void alloc_point_3d(double x,double y,double z){ +void +alloc_point_3d(double x,double y,double z) +{ tuple* p = alloc_tuple(write_point_3,the_geom.lwgi?12:24); p->points[0] = x; p->points[1] = y; @@ -374,7 +421,9 @@ void alloc_point_3d(double x,double y,double z){ check_dims(3); } -void alloc_point_4d(double x,double y,double z,double m){ +void +alloc_point_4d(double x,double y,double z,double m) +{ tuple* p = alloc_tuple(write_point_4,the_geom.lwgi?16:32); p->points[0] = x; p->points[1] = y; @@ -384,7 +433,9 @@ void alloc_point_4d(double x,double y,double z,double m){ check_dims(4); } -void write_type(tuple* this,output_state* out){ +void +write_type(tuple* this,output_state* out) +{ byte type=0; //Empty handler - switch back @@ -395,15 +446,7 @@ void write_type(tuple* this,output_state* out){ if (the_geom.ndims) //Support empty { - if ( the_geom.ndims == 3 ) - { - TYPE_SETZM(type, 1, 0); - } - if ( the_geom.ndims == 4 ) - { - TYPE_SETZM(type, 1, 1); - } - //type |= ((the_geom.ndims-2) << 4); + TYPE_SETZM(type, the_geom.hasZ, the_geom.hasM); } if ( the_geom.srid != -1 ){ @@ -420,17 +463,23 @@ void write_type(tuple* this,output_state* out){ } } -void write_count(tuple* this,output_state* out){ +void +write_count(tuple* this,output_state* out) +{ int num = this->num; WRITE_INT4(out,num); } -void write_type_count(tuple* this,output_state* out){ +void +write_type_count(tuple* this,output_state* out) +{ write_type(this,out); write_count(this,out); } -void alloc_point(void){ +void +alloc_point(void) +{ if( the_geom.lwgi) alloc_stack_tuple(POINTTYPEI,write_type,1); else @@ -439,7 +488,9 @@ void alloc_point(void){ minpoints=1; } -void alloc_linestring(void){ +void +alloc_linestring(void) +{ if( the_geom.lwgi) alloc_stack_tuple(LINETYPEI,write_type,1); else @@ -448,7 +499,9 @@ void alloc_linestring(void){ minpoints=2; } -void alloc_polygon(void){ +void +alloc_polygon(void) +{ if( the_geom.lwgi) alloc_stack_tuple(POLYGONTYPEI, write_type,1); else @@ -457,27 +510,39 @@ void alloc_polygon(void){ minpoints=3; } -void alloc_multipoint(void){ +void +alloc_multipoint(void) +{ alloc_stack_tuple(MULTIPOINTTYPE,write_type,1); } -void alloc_multilinestring(void){ +void +alloc_multilinestring(void) +{ alloc_stack_tuple(MULTILINETYPE,write_type,1); } -void alloc_multipolygon(void){ +void +alloc_multipolygon(void) +{ alloc_stack_tuple(MULTIPOLYGONTYPE,write_type,1); } -void alloc_geomertycollection(void){ +void +alloc_geomertycollection(void) +{ alloc_stack_tuple(COLLECTIONTYPE,write_type,1); } -void alloc_counter(void){ +void +alloc_counter(void) +{ alloc_stack_tuple(0,write_count,4); } -void alloc_empty(){ +void +alloc_empty() +{ tuple* st = the_geom.stack; //Find the last geometry while(st->type == 0){ @@ -504,7 +569,9 @@ void alloc_empty(){ } -byte* make_lwgeom(){ +byte * +make_lwgeom() +{ byte* out_c; output_state out; tuple* cur; @@ -524,7 +591,9 @@ byte* make_lwgeom(){ return out_c; } -int lwg_parse_yyerror(char* s){ +int +lwg_parse_yyerror(char* s) +{ error("parse error - invalid geometry"); //error_func("parse error - invalid geometry"); return 1; @@ -556,7 +625,9 @@ static const byte to_hex[] = { 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255}; -byte strhex_readbyte(const char* in){ +byte +strhex_readbyte(const char* in) +{ if ( *in == 0 ){ if ( ! ferror_occured){ error("invalid wkb"); @@ -566,7 +637,9 @@ byte strhex_readbyte(const char* in){ return to_hex[(int)*in]<<4 | to_hex[(int)*(in+1)]; } -byte read_wkb_byte(const char** in){ +byte +read_wkb_byte(const char** in) +{ byte ret = strhex_readbyte(*in); (*in)+=2; return ret; @@ -574,7 +647,9 @@ byte read_wkb_byte(const char** in){ int swap_order; -void read_wkb_bytes(const char** in,byte* out, int cnt){ +void +read_wkb_bytes(const char** in,byte* out, int cnt) +{ if ( ! swap_order ){ while(cnt--) *out++ = read_wkb_byte(in); } @@ -584,13 +659,17 @@ void read_wkb_bytes(const char** in,byte* out, int cnt){ } } -int4 read_wkb_int(const char** in){ +int4 +read_wkb_int(const char** in) +{ int4 ret; read_wkb_bytes(in,(byte*)&ret,4); return ret; } -double read_wbk_double(const char** in,int convert_from_int){ +double +read_wbk_double(const char** in,int convert_from_int) +{ double ret; if ( ! convert_from_int){ @@ -603,7 +682,9 @@ double read_wbk_double(const char** in,int convert_from_int){ } } -void read_wkb_point(const char** b){ +void +read_wkb_point(const char** b) +{ int i; tuple* p = NULL; @@ -639,7 +720,9 @@ void read_wkb_point(const char** b){ check_dims(the_geom.ndims); } -void read_collection(const char** b,read_col_func f){ +void +read_collection(const char** b,read_col_func f) +{ int4 cnt=read_wkb_int(b); alloc_counter(); @@ -651,11 +734,15 @@ void read_collection(const char** b,read_col_func f){ pop(); } -void read_collection2(const char** b){ +void +read_collection2(const char** b) +{ return read_collection(b,read_wkb_point); } -void parse_wkb(const char** b){ +void +parse_wkb(const char** b) +{ int4 type; byte xdr = read_wkb_byte(b); swap_order=0; @@ -671,17 +758,22 @@ void parse_wkb(const char** b){ type = read_wkb_int(b); - - - //quick exit on error if ( ferror_occured ) return; the_geom.ndims=2; if (type & WKBZOFFSET) - the_geom.ndims=3; + { + the_geom.hasZ = 1; + the_geom.ndims++; + } + else the_geom.hasZ = 0; if (type & WKBMOFFSET) - the_geom.ndims=4; + { + the_geom.hasM = 1; + the_geom.ndims++; + } + else the_geom.hasM = 0; type &=0x0f; @@ -746,14 +838,18 @@ void parse_wkb(const char** b){ } -void alloc_wkb(const char* parser){ +void +alloc_wkb(const char* parser) +{ parse_wkb(&parser); } /* Parse a string and return a LW_GEOM */ -byte* parse_it(const char* geometry,allocator allocfunc,report_error errfunc){ +byte * +parse_it(const char* geometry,allocator allocfunc,report_error errfunc) +{ local_malloc = allocfunc; error_func=errfunc; @@ -770,15 +866,24 @@ byte* parse_it(const char* geometry,allocator allocfunc,report_error errfunc){ return make_lwgeom(); } -byte* parse_lwg(const char* geometry,allocator allocfunc,report_error errfunc){ +byte * +parse_lwg(const char* geometry,allocator allocfunc,report_error errfunc) +{ the_geom.lwgi=0; return parse_it(geometry,allocfunc,errfunc); } -byte* parse_lwgi(const char* geometry,allocator allocfunc,report_error errfunc){ +byte * +parse_lwgi(const char* geometry,allocator allocfunc,report_error errfunc) +{ the_geom.lwgi=1; return parse_it(geometry,allocfunc,errfunc); } - - +void +set_zm(char z, char m) +{ + the_geom.hasZ = z; + the_geom.hasM = m; + the_geom.ndims = 2+z+m; +} diff --git a/lwgeom/wktparse.lex b/lwgeom/wktparse.lex index ae694ac06..e5ee602d6 100644 --- a/lwgeom/wktparse.lex +++ b/lwgeom/wktparse.lex @@ -30,12 +30,19 @@ static YY_BUFFER_STATE buf_state; 01[0-9A-F]* { lwg_parse_yylval.wkb=lwg_parse_yytext; return WKB;} <*>POINT { return POINT; } +<*>POINTM { return POINTM; } <*>LINESTRING { return LINESTRING; } +<*>LINESTRINGM { return LINESTRINGM; } <*>POLYGON { return POLYGON; } +<*>POLYGONM { return POLYGONM; } <*>MULTIPOINT { return MULTIPOINT; } +<*>MULTIPOINTM { return MULTIPOINTM; } <*>MULTILINESTRING { return MULTILINESTRING; } +<*>MULTILINESTRINGM { return MULTILINESTRINGM; } <*>MULTIPOLYGON { return MULTIPOLYGON; } +<*>MULTIPOLYGONM { return MULTIPOLYGONM; } <*>GEOMETRYCOLLECTION { return GEOMETRYCOLLECTION; } +<*>GEOMETRYCOLLECTIONM { return GEOMETRYCOLLECTIONM; } <*>SRID { BEGIN(vals_ok); return SRID; } <*>EMPTY { return EMPTY; } diff --git a/lwgeom/wktparse.y b/lwgeom/wktparse.y index eec4961a1..6cc24ccd6 100644 --- a/lwgeom/wktparse.y +++ b/lwgeom/wktparse.y @@ -19,6 +19,7 @@ } %token POINT LINESTRING POLYGON MULTIPOINT MULTILINESTRING MULTIPOLYGON GEOMETRYCOLLECTION +%token POINTM LINESTRINGM POLYGONM MULTIPOINTM MULTILINESTRINGM MULTIPOLYGONM GEOMETRYCOLLECTIONM %token SRID %token EMPTY %token VALUE @@ -40,7 +41,7 @@ geom_wkb : WKB {alloc_wkb($1) ; } ; /* POINT */ -geom_point : POINT point ; +geom_point : POINT point | POINTM { set_zm(0, 1); } point ; point : { alloc_point(); } point_int { pop();} ; @@ -48,7 +49,8 @@ point_int : empty | LPAREN a_point RPAREN; /* MULTIPOINT */ -geom_multipoint : MULTIPOINT { alloc_multipoint(); } multipoint { pop();}; +geom_multipoint : MULTIPOINT { alloc_multipoint(); } multipoint { pop();} | + MULTIPOINTM { set_zm(0, 1); alloc_multipoint(); } multipoint {pop();}; multipoint : empty | { alloc_counter(); } LPAREN multipoint_int RPAREN {pop();} ; @@ -59,7 +61,7 @@ mpoint : point | { alloc_point(); } a_point { pop();} ; /* LINESTRING */ -geom_linestring : LINESTRING linestring; +geom_linestring : LINESTRING linestring | LINESTRINGM { set_zm(0, 1); } linestring ; linestring : { alloc_linestring(); } linestring_1 {pop();} ; @@ -69,7 +71,7 @@ linestring_int : a_point | linestring_int COMMA a_point; /* MULTILINESTRING */ -geom_multilinestring : MULTILINESTRING { alloc_multilinestring(); } multilinestring { pop();} ; +geom_multilinestring : MULTILINESTRING { alloc_multilinestring(); } multilinestring { pop();} | MULTILINESTRINGM { set_zm(0, 1); alloc_multilinestring(); } multilinestring { pop(); } ; multilinestring : empty | { alloc_counter(); } LPAREN multilinestring_int RPAREN{ pop();} ; @@ -78,7 +80,7 @@ multilinestring_int : linestring | multilinestring_int COMMA linestring; /* POLYGON */ -geom_polygon : POLYGON polygon; +geom_polygon : POLYGON polygon | POLYGONM { set_zm(0, 1); } polygon ; polygon : { alloc_polygon(); } polygon_1 { pop();} ; @@ -88,7 +90,7 @@ polygon_int : linestring_1 | polygon_int COMMA linestring_1; /* MULTIPOLYGON */ -geom_multipolygon : MULTIPOLYGON { alloc_multipolygon(); } multipolygon { pop();}; +geom_multipolygon : MULTIPOLYGON { alloc_multipolygon(); } multipolygon { pop();} | MULTIPOLYGONM { set_zm(0, 1); alloc_multipolygon(); } multipolygon { pop();} ; multipolygon : empty | { alloc_counter(); } LPAREN multipolygon_int RPAREN { pop();} ; @@ -98,7 +100,7 @@ multipolygon_int : polygon | multipolygon_int COMMA polygon; /* GEOMETRYCOLLECTION */ -geom_geometrycollection : GEOMETRYCOLLECTION { alloc_geomertycollection(); } geometrycollection { pop();} ; +geom_geometrycollection : GEOMETRYCOLLECTION { alloc_geomertycollection(); } geometrycollection { pop();} | GEOMETRYCOLLECTIONM { set_zm(0, 1); alloc_geomertycollection(); } geometrycollection { pop();} ; geometrycollection : empty | { alloc_counter(); } LPAREN geometrycollection_int RPAREN { pop();} ; diff --git a/lwgeom/wktunparse.c b/lwgeom/wktunparse.c index c1e4841e1..7dc1d5f51 100644 --- a/lwgeom/wktunparse.c +++ b/lwgeom/wktunparse.c @@ -167,14 +167,18 @@ byte* output_point(byte* geom,int supress){ return geom; } -byte* output_single(byte* geom,int supress){ +byte * +output_single(byte* geom,int supress) +{ write_str("("); geom=output_point(geom,supress); write_str(")"); return geom; } -byte* output_collection(byte* geom,outfunc func,int supress){ +byte * +output_collection(byte* geom,outfunc func,int supress) +{ int cnt = read_int(&geom); if ( cnt == 0 ){ write_str(" EMPTY"); @@ -192,14 +196,16 @@ byte* output_collection(byte* geom,outfunc func,int supress){ return geom; } -byte* output_collection_2(byte* geom,int suppress){ +byte * +output_collection_2(byte* geom,int suppress) +{ return output_collection(geom,output_point,suppress); } -byte* output_wkt(byte* geom, int supress); +byte *output_wkt(byte* geom, int supress); /* special case for multipoint to supress extra brackets */ -byte* output_multipoint(byte* geom,int suppress){ +byte *output_multipoint(byte* geom,int suppress){ unsigned type = *geom & 0x0f; if ( type == POINTTYPE ) @@ -214,70 +220,114 @@ byte* output_multipoint(byte* geom,int suppress){ return output_wkt(geom,suppress); } +// Suppress=0 // write TYPE, M, coords +// Suppress=1 // write TYPE, coords +// Suppress=2 // write only coords byte * output_wkt(byte* geom, int supress) { unsigned type=*geom++; dims = TYPE_NDIMS(type); //((type & 0x30) >> 4)+2; + char writeM=0; + + if ( ! supress && !TYPE_HASZ(type) && TYPE_HASM(type) ) writeM=1; + //Skip the bounding box if there is one - //if ( type & 0x80 ){ if ( TYPE_HASBBOX(type) ) { geom+=16; } - //if ( type & 0x40 ){ if ( TYPE_HASSRID(type) ) { write_str("SRID=");write_int(read_int(&geom));write_str(";"); } - //switch(type & 0x0F){ switch(TYPE_GETTYPE(type)) { case POINTTYPE: - if ( ! supress) write_str("POINT"); + if ( supress < 2 ) + { + if (writeM) write_str("POINTM"); + else write_str("POINT"); + } geom=output_single(geom,0); break; case LINETYPE: - if ( ! supress) write_str("LINESTRING"); + if ( supress < 2 ) + { + if (writeM) write_str("LINESTRINGM"); + else write_str("LINESTRING"); + } geom = output_collection(geom,output_point,0); break; case POLYGONTYPE: - if ( ! supress) write_str("POLYGON"); + if ( supress < 2 ) + { + if (writeM) write_str("POLYGONM"); + else write_str("POLYGON"); + } geom = output_collection(geom,output_collection_2,0); break; case MULTIPOINTTYPE: - if ( ! supress) write_str("MULTIPOINT"); - geom = output_collection(geom,output_multipoint,1); + if ( supress < 2 ) + { + if (writeM) write_str("MULTIPOINTM"); + else write_str("MULTIPOINT"); + } + geom = output_collection(geom,output_multipoint,2); break; case MULTILINETYPE: - if ( ! supress) write_str("MULTILINESTRING"); - geom = output_collection(geom,output_wkt,1); + if ( supress < 2 ) + { + if (writeM) write_str("MULTILINESTRINGM"); + else write_str("MULTILINESTRING"); + } + geom = output_collection(geom,output_wkt,2); break; case MULTIPOLYGONTYPE: - if ( ! supress) write_str("MULTIPOLYGON"); - geom = output_collection(geom,output_wkt,1); + if ( supress < 2 ) + { + if (writeM) write_str("MULTIPOLYGONM"); + else write_str("MULTIPOLYGON"); + } + geom = output_collection(geom,output_wkt,2); break; case COLLECTIONTYPE: - if ( ! supress) write_str("GEOMETRYCOLLECTION"); - geom = output_collection(geom,output_wkt,0); + if ( supress < 2 ) + { + if (writeM) write_str("GEOMETRYCOLLECTIONM"); + else write_str("GEOMETRYCOLLECTION"); + } + geom = output_collection(geom,output_wkt,1); break; case POINTTYPEI: - if ( ! supress) write_str("POINT"); + if ( supress < 2 ) + { + if (writeM) write_str("POINTM"); + else write_str("POINT"); + } lwgi++; geom=output_single(geom,0); lwgi--; break; case LINETYPEI: - if ( ! supress) write_str("LINESTRING"); + if ( supress < 2 ) + { + if (writeM) write_str("LINESTRINGM"); + else write_str("LINESTRING"); + } lwgi++; geom = output_collection(geom,output_point,0); lwgi--; break; case POLYGONTYPEI: - if ( ! supress) write_str("POLYGON"); + if ( supress < 2 ) + { + if (writeM) write_str("POLYGONM"); + else write_str("POLYGON"); + } lwgi++; geom =output_collection(geom,output_collection_2,0); lwgi--; @@ -429,7 +479,9 @@ output_wkb(byte* geom) return geom; } -char* unparse_WKB(byte* lw_geom,allocator alloc,freeor free){ +char * +unparse_WKB(byte* lw_geom,allocator alloc,freeor free) +{ if (lw_geom==NULL) return NULL;