1 /// 2 module dpq2.conv.geometric; 3 4 import dpq2.oids: OidType; 5 import dpq2.value: ConvExceptionType, throwTypeComplaint, Value, ValueConvException, ValueFormat; 6 import std.bitmanip: bigEndianToNative, nativeToBigEndian; 7 import std.traits; 8 import std.range.primitives: ElementType; 9 10 @safe: 11 12 private template GetRvalueOfMember(T, string memberName) 13 { 14 mixin("alias MemberType = typeof(T."~memberName~");"); 15 16 static if(is(MemberType == function)) 17 alias R = ReturnType!(MemberType); 18 else 19 alias R = MemberType; 20 21 alias GetRvalueOfMember = R; 22 } 23 24 /// Checks that type have "x" and "y" members of returning type "double" 25 bool isValidPointType(T)() 26 { 27 static if(__traits(compiles, typeof(T.x)) && __traits(compiles, typeof(T.y))) 28 { 29 return 30 is(GetRvalueOfMember!(T, "x") == double) && 31 is(GetRvalueOfMember!(T, "y") == double); 32 } 33 else 34 return false; 35 } 36 37 unittest 38 { 39 { 40 struct PT {double x; double y;} 41 assert(isValidPointType!PT); 42 } 43 44 { 45 struct InvalidPT {double x;} 46 assert(!isValidPointType!InvalidPT); 47 } 48 } 49 50 /// Checks that type have "min" and "max" members of suitable returning type of point 51 bool isValidBoxType(T)() 52 { 53 static if(__traits(compiles, typeof(T.min)) && __traits(compiles, typeof(T.max))) 54 { 55 return 56 isValidPointType!(GetRvalueOfMember!(T, "min")) && 57 isValidPointType!(GetRvalueOfMember!(T, "max")); 58 } 59 else 60 return false; 61 } 62 63 /// 64 bool isValidLineSegmentType(T)() 65 { 66 static if(__traits(compiles, typeof(T.start)) && __traits(compiles, typeof(T.end))) 67 { 68 return 69 isValidPointType!(GetRvalueOfMember!(T, "start")) && 70 isValidPointType!(GetRvalueOfMember!(T, "end")); 71 } 72 else 73 return false; 74 } 75 76 /// 77 bool isValidPolygon(T)() 78 { 79 return isArray!T && isValidPointType!(ElementType!T); 80 } 81 82 unittest 83 { 84 struct PT {double x; double y;} 85 assert(isValidPolygon!(PT[])); 86 assert(!isValidPolygon!(PT)); 87 } 88 89 private auto serializePoint(Vec2Ddouble, T)(Vec2Ddouble point, T target) 90 if(isValidPointType!Vec2Ddouble) 91 { 92 import std.algorithm : copy; 93 94 auto rem = point.x.nativeToBigEndian.copy(target); 95 rem = point.y.nativeToBigEndian.copy(rem); 96 97 return rem; 98 } 99 100 Value toValue(Vec2Ddouble)(Vec2Ddouble pt) 101 if(isValidPointType!Vec2Ddouble) 102 { 103 ubyte[] data = new ubyte[16]; 104 pt.serializePoint(data); 105 106 return createValue(data, OidType.Point); 107 } 108 109 private auto serializeBox(Box, T)(Box box, T target) 110 { 111 auto rem = box.max.serializePoint(target); 112 rem = box.min.serializePoint(rem); 113 114 return rem; 115 } 116 117 Value toValue(Box)(Box box) 118 if(isValidBoxType!Box) 119 { 120 ubyte[] data = new ubyte[32]; 121 box.serializeBox(data); 122 123 return createValue(data, OidType.Box); 124 } 125 126 /// Infinite line - {A,B,C} (Ax + By + C = 0) 127 struct Line 128 { 129 double a; /// 130 double b; /// 131 double c; /// 132 } 133 134 /// 135 struct Path(Point) 136 if(isValidPointType!Point) 137 { 138 bool isClosed; /// 139 Point[] points; /// 140 } 141 142 /// 143 struct Circle(Point) 144 if(isValidPointType!Point) 145 { 146 Point center; /// 147 double radius; /// 148 } 149 150 Value toValue(T)(T line) 151 if(is(T == Line)) 152 { 153 import std.algorithm : copy; 154 155 ubyte[] data = new ubyte[24]; 156 157 auto rem = line.a.nativeToBigEndian.copy(data); 158 rem = line.b.nativeToBigEndian.copy(rem); 159 rem = line.c.nativeToBigEndian.copy(rem); 160 161 return createValue(data, OidType.Line); 162 } 163 164 Value toValue(LineSegment)(LineSegment lseg) 165 if(isValidLineSegmentType!LineSegment) 166 { 167 ubyte[] data = new ubyte[32]; 168 169 auto rem = lseg.start.serializePoint(data); 170 rem = lseg.end.serializePoint(rem); 171 172 return createValue(data, OidType.LineSegment); 173 } 174 175 Value toValue(T)(T path) 176 if(isInstanceOf!(Path, T)) 177 { 178 import std.algorithm : copy; 179 180 if(path.points.length < 1) 181 throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 182 "At least one point is needed for Path", __FILE__, __LINE__); 183 184 ubyte[] data = new ubyte[path.points.length * 16 + 5]; 185 186 auto rem = (cast(ubyte)(path.isClosed ? 1 : 0)).nativeToBigEndian.copy(data); 187 rem = (cast(int)path.points.length).nativeToBigEndian.copy(rem); 188 189 foreach (ref p; path.points) 190 { 191 rem = p.serializePoint(rem); 192 } 193 194 return createValue(data, OidType.Path); 195 } 196 197 Value toValue(Polygon)(Polygon poly) 198 if(isValidPolygon!Polygon) 199 { 200 import std.algorithm : copy; 201 202 if(poly.length < 1) 203 throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 204 "At least one point is needed for Polygon", __FILE__, __LINE__); 205 206 ubyte[] data = new ubyte[poly.length * 16 + 4]; 207 auto rem = (cast(int)poly.length).nativeToBigEndian.copy(data); 208 209 foreach (ref p; poly) 210 rem = p.serializePoint(rem); 211 212 return createValue(data, OidType.Polygon); 213 } 214 215 Value toValue(T)(T c) 216 if(isInstanceOf!(Circle, T)) 217 { 218 import std.algorithm : copy; 219 220 ubyte[] data = new ubyte[24]; 221 auto rem = c.center.serializePoint(data); 222 c.radius.nativeToBigEndian.copy(rem); 223 224 return createValue(data, OidType.Circle); 225 } 226 227 /// Caller must ensure that reference to the data will not be passed to elsewhere 228 private Value createValue(const ubyte[] data, OidType oid) pure @trusted 229 { 230 return Value(cast(immutable) data, oid); 231 } 232 233 private alias AE = ValueConvException; 234 private alias ET = ConvExceptionType; 235 236 /// Convert to Point 237 Vec2Ddouble binaryValueAs(Vec2Ddouble)(in Value v) 238 if(isValidPointType!Vec2Ddouble) 239 { 240 if(!(v.oidType == OidType.Point)) 241 throwTypeComplaint(v.oidType, "Point", __FILE__, __LINE__); 242 243 auto data = v.data; 244 245 if(!(data.length == 16)) 246 throw new AE(ET.SIZE_MISMATCH, 247 "Value length isn't equal to Postgres Point size", __FILE__, __LINE__); 248 249 return pointFromBytes!Vec2Ddouble(data[0..16]); 250 } 251 252 private Vec2Ddouble pointFromBytes(Vec2Ddouble)(in ubyte[16] data) pure 253 if(isValidPointType!Vec2Ddouble) 254 { 255 return Vec2Ddouble(data[0..8].bigEndianToNative!double, data[8..16].bigEndianToNative!double); 256 } 257 258 T binaryValueAs(T)(in Value v) 259 if (is(T == Line)) 260 { 261 if(!(v.oidType == OidType.Line)) 262 throwTypeComplaint(v.oidType, "Line", __FILE__, __LINE__); 263 264 if(!(v.data.length == 24)) 265 throw new AE(ET.SIZE_MISMATCH, 266 "Value length isn't equal to Postgres Line size", __FILE__, __LINE__); 267 268 return Line((v.data[0..8].bigEndianToNative!double), v.data[8..16].bigEndianToNative!double, v.data[16..24].bigEndianToNative!double); 269 } 270 271 LineSegment binaryValueAs(LineSegment)(in Value v) 272 if(isValidLineSegmentType!LineSegment) 273 { 274 if(!(v.oidType == OidType.LineSegment)) 275 throwTypeComplaint(v.oidType, "LineSegment", __FILE__, __LINE__); 276 277 if(!(v.data.length == 32)) 278 throw new AE(ET.SIZE_MISMATCH, 279 "Value length isn't equal to Postgres LineSegment size", __FILE__, __LINE__); 280 281 alias Point = ReturnType!(LineSegment.start); 282 283 auto start = v.data[0..16].pointFromBytes!Point; 284 auto end = v.data[16..32].pointFromBytes!Point; 285 286 return LineSegment(start, end); 287 } 288 289 Box binaryValueAs(Box)(in Value v) 290 if(isValidBoxType!Box) 291 { 292 if(!(v.oidType == OidType.Box)) 293 throwTypeComplaint(v.oidType, "Box", __FILE__, __LINE__); 294 295 if(!(v.data.length == 32)) 296 throw new AE(ET.SIZE_MISMATCH, 297 "Value length isn't equal to Postgres Box size", __FILE__, __LINE__); 298 299 alias Point = typeof(Box.min); 300 301 Box res; 302 res.max = v.data[0..16].pointFromBytes!Point; 303 res.min = v.data[16..32].pointFromBytes!Point; 304 305 return res; 306 } 307 308 T binaryValueAs(T)(in Value v) 309 if(isInstanceOf!(Path, T)) 310 { 311 import std.array : uninitializedArray; 312 313 if(!(v.oidType == OidType.Path)) 314 throwTypeComplaint(v.oidType, "Path", __FILE__, __LINE__); 315 316 if(!((v.data.length - 5) % 16 == 0)) 317 throw new AE(ET.SIZE_MISMATCH, 318 "Value length isn't equal to Postgres Path size", __FILE__, __LINE__); 319 320 T res; 321 res.isClosed = v.data[0..1].bigEndianToNative!byte == 1; 322 int len = v.data[1..5].bigEndianToNative!int; 323 324 if (len != (v.data.length - 5)/16) 325 throw new AE(ET.SIZE_MISMATCH, "Path points number mismatch", __FILE__, __LINE__); 326 327 alias Point = typeof(T.points[0]); 328 329 res.points = uninitializedArray!(Point[])(len); 330 for (int i=0; i<len; i++) 331 { 332 const ubyte[] b = v.data[ i*16+5 .. i*16+5+16 ]; 333 res.points[i] = b[0..16].pointFromBytes!Point; 334 } 335 336 return res; 337 } 338 339 Polygon binaryValueAs(Polygon)(in Value v) 340 if(isValidPolygon!Polygon) 341 { 342 import std.array : uninitializedArray; 343 344 if(!(v.oidType == OidType.Polygon)) 345 throwTypeComplaint(v.oidType, "Polygon", __FILE__, __LINE__); 346 347 if(!((v.data.length - 4) % 16 == 0)) 348 throw new AE(ET.SIZE_MISMATCH, 349 "Value length isn't equal to Postgres Polygon size", __FILE__, __LINE__); 350 351 Polygon res; 352 int len = v.data[0..4].bigEndianToNative!int; 353 354 if (len != (v.data.length - 4)/16) 355 throw new AE(ET.SIZE_MISMATCH, "Path points number mismatch", __FILE__, __LINE__); 356 357 alias Point = ElementType!Polygon; 358 359 res = uninitializedArray!(Point[])(len); 360 for (int i=0; i<len; i++) 361 { 362 const ubyte[] b = v.data[(i*16+4)..(i*16+16+4)]; 363 res[i] = b[0..16].pointFromBytes!Point; 364 } 365 366 return res; 367 } 368 369 T binaryValueAs(T)(in Value v) 370 if(isInstanceOf!(Circle, T)) 371 { 372 if(!(v.oidType == OidType.Circle)) 373 throwTypeComplaint(v.oidType, "Circle", __FILE__, __LINE__); 374 375 if(!(v.data.length == 24)) 376 throw new AE(ET.SIZE_MISMATCH, 377 "Value length isn't equal to Postgres Circle size", __FILE__, __LINE__); 378 379 alias Point = typeof(T.center); 380 381 return T( 382 v.data[0..16].pointFromBytes!Point, 383 v.data[16..24].bigEndianToNative!double 384 ); 385 } 386 387 version (integration_tests) 388 package mixin template GeometricInstancesForIntegrationTest() 389 { 390 @safe: 391 392 import gfm.math; 393 import dpq2.conv.geometric: Circle, Path; 394 395 alias Point = vec2d; 396 alias Box = box2d; 397 static struct LineSegment 398 { 399 seg2d seg; 400 alias seg this; 401 402 ref Point start(){ return a; } 403 ref Point end(){ return b; } 404 405 this(Point a, Point b) 406 { 407 seg.a = a; 408 seg.b = b; 409 } 410 } 411 alias TestPath = Path!Point; 412 alias Polygon = Point[]; 413 alias TestCircle = Circle!Point; 414 } 415 416 version (integration_tests) 417 unittest 418 { 419 mixin GeometricInstancesForIntegrationTest; 420 421 // binary write/read 422 { 423 auto pt = Point(1,2); 424 assert(pt.toValue.binaryValueAs!Point == pt); 425 426 auto ln = Line(1,2,3); 427 assert(ln.toValue.binaryValueAs!Line == ln); 428 429 auto lseg = LineSegment(Point(1,2),Point(3,4)); 430 assert(lseg.toValue.binaryValueAs!LineSegment == lseg); 431 432 auto b = Box(Point(2,2), Point(1,1)); 433 assert(b.toValue.binaryValueAs!Box == b); 434 435 auto p = TestPath(false, [Point(1,1), Point(2,2)]); 436 assert(p.toValue.binaryValueAs!TestPath == p); 437 438 p = TestPath(true, [Point(1,1), Point(2,2)]); 439 assert(p.toValue.binaryValueAs!TestPath == p); 440 441 Polygon poly = [Point(1,1), Point(2,2), Point(3,3)]; 442 assert(poly.toValue.binaryValueAs!Polygon == poly); 443 444 auto c = TestCircle(Point(1,2), 3); 445 assert(c.toValue.binaryValueAs!TestCircle == c); 446 } 447 448 // Invalid OID tests 449 { 450 import std.exception : assertThrown; 451 452 auto v = Point(1,1).toValue; 453 v.oidType = OidType.Text; 454 assertThrown!ValueConvException(v.binaryValueAs!Point); 455 456 v = Line(1,2,3).toValue; 457 v.oidType = OidType.Text; 458 assertThrown!ValueConvException(v.binaryValueAs!Line); 459 460 v = LineSegment(Point(1,1), Point(2,2)).toValue; 461 v.oidType = OidType.Text; 462 assertThrown!ValueConvException(v.binaryValueAs!LineSegment); 463 464 v = Box(Point(1,1), Point(2,2)).toValue; 465 v.oidType = OidType.Text; 466 assertThrown!ValueConvException(v.binaryValueAs!Box); 467 468 v = TestPath(true, [Point(1,1), Point(2,2)]).toValue; 469 v.oidType = OidType.Text; 470 assertThrown!ValueConvException(v.binaryValueAs!TestPath); 471 472 v = [Point(1,1), Point(2,2)].toValue; 473 v.oidType = OidType.Text; 474 assertThrown!ValueConvException(v.binaryValueAs!Polygon); 475 476 v = TestCircle(Point(1,1), 3).toValue; 477 v.oidType = OidType.Text; 478 assertThrown!ValueConvException(v.binaryValueAs!TestCircle); 479 } 480 481 // Invalid data size 482 { 483 import std.exception : assertThrown; 484 485 auto v = Point(1,1).toValue; 486 v._data = new ubyte[1]; 487 assertThrown!ValueConvException(v.binaryValueAs!Point); 488 489 v = Line(1,2,3).toValue; 490 v._data.length = 1; 491 assertThrown!ValueConvException(v.binaryValueAs!Line); 492 493 v = LineSegment(Point(1,1), Point(2,2)).toValue; 494 v._data.length = 1; 495 assertThrown!ValueConvException(v.binaryValueAs!LineSegment); 496 497 v = Box(Point(1,1), Point(2,2)).toValue; 498 v._data.length = 1; 499 assertThrown!ValueConvException(v.binaryValueAs!Box); 500 501 v = TestPath(true, [Point(1,1), Point(2,2)]).toValue; 502 v._data.length -= 16; 503 assertThrown!ValueConvException(v.binaryValueAs!TestPath); 504 v._data.length = 1; 505 assertThrown!ValueConvException(v.binaryValueAs!TestPath); 506 507 v = [Point(1,1), Point(2,2)].toValue; 508 v._data.length -= 16; 509 assertThrown!ValueConvException(v.binaryValueAs!Polygon); 510 v._data.length = 1; 511 assertThrown!ValueConvException(v.binaryValueAs!Polygon); 512 513 v = TestCircle(Point(1,1), 3).toValue; 514 v._data.length = 1; 515 assertThrown!ValueConvException(v.binaryValueAs!TestCircle); 516 } 517 }