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 }