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 }