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