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