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 }