1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace LaravelSpatial\Eloquent; |
4
|
|
|
|
5
|
|
|
use Exception; |
6
|
|
|
use GeoJson\GeoJson; |
7
|
|
|
use GeoJSON\Geometry\Geometry; |
8
|
|
|
use geoPHP\geoPHP; |
9
|
|
|
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; |
10
|
|
|
use Illuminate\Database\MySqlConnection; |
11
|
|
|
use Illuminate\Database\PostgresConnection; |
12
|
|
|
use LaravelSpatial\Exceptions\SpatialFieldsNotDefinedException; |
13
|
|
|
use LaravelSpatial\Exceptions\SpatialParseException; |
14
|
|
|
use LaravelSpatial\Exceptions\UnknownSpatialRelationFunction; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Trait SpatialTrait. |
18
|
|
|
* |
19
|
|
|
* @property array $attributes |
20
|
|
|
* @method static static|EloquentBuilder distance($geometryColumn, $geometry, $distance) |
21
|
|
|
* @method static static|EloquentBuilder distanceValue($geometryColumn, $geometry) |
22
|
|
|
* @method static static|EloquentBuilder distanceExcludingSelf($geometryColumn, $geometry, $distance) |
23
|
|
|
* @method static static|EloquentBuilder distanceSphere($geometryColumn, $geometry, $distance) |
24
|
|
|
* @method static static|EloquentBuilder distanceSphereValue($geometryColumn, $geometry) |
25
|
|
|
* @method static static|EloquentBuilder distanceSphereExcludingSelf($geometryColumn, $geometry, $distance) |
26
|
|
|
* @method static static|EloquentBuilder comparison($geometryColumn, $geometry, $relationship) |
27
|
|
|
* @method static static|EloquentBuilder within($geometryColumn, $polygon) |
28
|
|
|
* @method static static|EloquentBuilder crosses($geometryColumn, $geometry) |
29
|
|
|
* @method static static|EloquentBuilder contains($geometryColumn, $geometry) |
30
|
|
|
* @method static static|EloquentBuilder disjoint($geometryColumn, $geometry) |
31
|
|
|
* @method static static|EloquentBuilder equals($geometryColumn, $geometry) |
32
|
|
|
* @method static static|EloquentBuilder intersects($geometryColumn, $geometry) |
33
|
|
|
* @method static static|EloquentBuilder overlaps($geometryColumn, $geometry) |
34
|
|
|
* @method static static|EloquentBuilder doesTouch($geometryColumn, $geometry) |
35
|
|
|
*/ |
36
|
|
|
trait SpatialTrait |
37
|
|
|
{ |
38
|
|
|
/* |
39
|
|
|
* The attributes that are spatial representations. |
40
|
|
|
* To use this Trait, add the following array to the model class |
41
|
|
|
* |
42
|
|
|
* @var array |
43
|
|
|
* |
44
|
|
|
* protected $spatialFields = []; |
45
|
|
|
*/ |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @var array |
49
|
|
|
*/ |
50
|
|
|
public $geometries = []; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @var array |
54
|
|
|
*/ |
55
|
|
|
protected $stRelations = [ |
56
|
|
|
'Within', |
57
|
|
|
'Crosses', |
58
|
|
|
'Contains', |
59
|
|
|
'Disjoint', |
60
|
|
|
'Equals', |
61
|
|
|
'Intersects', |
62
|
|
|
'Overlaps', |
63
|
|
|
'Touches', |
64
|
|
|
]; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Create a new Eloquent query builder for the model. |
68
|
|
|
* |
69
|
|
|
* @param \Illuminate\Database\Query\Builder $query |
70
|
|
|
* |
71
|
|
|
* @return \LaravelSpatial\Eloquent\Builder |
72
|
|
|
*/ |
73
|
48 |
|
public function newEloquentBuilder($query) |
74
|
|
|
{ |
75
|
48 |
|
return new Builder($query); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @inheritDoc |
80
|
|
|
*/ |
81
|
11 |
|
public function setRawAttributes(array $attributes, $sync = false) |
82
|
|
|
{ |
83
|
11 |
|
$spatial_fields = $this->getSpatialFields(); |
84
|
|
|
|
85
|
11 |
|
foreach ($attributes as $attribute => &$value) { |
86
|
11 |
|
if (is_string($value) && in_array($attribute, $spatial_fields, true) && strlen($value) >= 15) { |
87
|
11 |
|
$connection = $this->getConnection(); |
88
|
|
|
|
89
|
|
|
// MySQL adds 4 NULL bytes at the start of the binary |
90
|
11 |
|
if ($connection instanceof MySqlConnection && strpos($value, "\0\0\0\0") === 0) { |
91
|
5 |
|
$value = substr($value, 4); |
92
|
6 |
|
} elseif ($connection instanceof PostgresConnection) { |
93
|
5 |
|
$value = pack('H*', $value); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
try { |
97
|
11 |
|
$value = GeoJson::jsonUnserialize(json_decode(geoPHP::load($value, 'wkb')->out('json'), false)); |
98
|
|
|
} catch (Exception $e) { |
99
|
|
|
throw new SpatialParseException("Can't parse WKB {$value}: {$e->getMessage()}", $e->getCode(), $e); |
100
|
|
|
} |
101
|
|
|
} |
102
|
|
|
} |
103
|
|
|
|
104
|
11 |
|
return parent::setRawAttributes($attributes, $sync); |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* @return array |
109
|
|
|
*/ |
110
|
50 |
|
public function getSpatialFields(): array |
111
|
|
|
{ |
112
|
50 |
|
if (property_exists($this, 'spatialFields') && !empty($this->spatialFields)) { |
113
|
47 |
|
return $this->spatialFields; |
114
|
|
|
} |
115
|
|
|
|
116
|
3 |
|
throw new SpatialFieldsNotDefinedException(__CLASS__ . ' has to define $spatialFields'); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* @param \GeoJSON\Geometry\Geometry $value |
121
|
|
|
* |
122
|
|
|
* @return string |
123
|
|
|
*/ |
124
|
46 |
|
protected function toWkt(Geometry $value): string |
125
|
|
|
{ |
126
|
|
|
try { |
127
|
46 |
|
$wkt = geoPHP::load(json_decode(json_encode($value->jsonSerialize()), false), 'json')->out('wkt'); |
128
|
|
|
} catch (Exception $e) { |
129
|
|
|
throw new SpatialParseException('Unable to data to geometry.', 0, $e); |
130
|
|
|
} |
131
|
|
|
|
132
|
46 |
|
return ($this->getConnection() instanceof PostgresConnection ? 'SRID=4326;' : '') . $wkt; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
137
|
|
|
* |
138
|
|
|
* @return bool |
139
|
|
|
*/ |
140
|
31 |
|
protected function performInsert(EloquentBuilder $query) |
141
|
|
|
{ |
142
|
31 |
|
foreach ($this->attributes as $key => $value) { |
143
|
31 |
|
if ($value instanceof Geometry && $this->isColumnAllowed($key)) { |
144
|
29 |
|
$this->geometries[$key] = $value; // Preserve the geometry objects prior to the insert |
145
|
29 |
|
$this->attributes[$key] = $this->getConnection()->raw("ST_GeomFromText('{$this->toWkt($value)}')"); |
146
|
|
|
} |
147
|
|
|
} |
148
|
|
|
|
149
|
29 |
|
$insert = parent::performInsert($query); |
150
|
|
|
|
151
|
29 |
|
foreach ($this->geometries as $key => $value) { |
152
|
29 |
|
$this->attributes[$key] = $value; // Retrieve the geometry objects so they can be used in the model |
153
|
|
|
} |
154
|
|
|
|
155
|
29 |
|
return $insert; // Return the result of the parent insert |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* @param $geometryColumn |
160
|
|
|
* |
161
|
|
|
* @return bool |
162
|
|
|
*/ |
163
|
48 |
|
public function isColumnAllowed($geometryColumn): bool |
164
|
|
|
{ |
165
|
48 |
|
if (!in_array($geometryColumn, $this->getSpatialFields(), true)) { |
166
|
|
|
throw new SpatialFieldsNotDefinedException(sprintf('%s is not a valid spatial column.', $geometryColumn)); |
167
|
|
|
} |
168
|
|
|
|
169
|
46 |
|
return true; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
174
|
|
|
* @param $geometryColumn |
175
|
|
|
* @param $geometry |
176
|
|
|
* @param $distance |
177
|
|
|
* |
178
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
179
|
|
|
*/ |
180
|
4 |
|
public function scopeDistance($query, $geometryColumn, $geometry, $distance): EloquentBuilder |
181
|
|
|
{ |
182
|
4 |
|
if ($this->isColumnAllowed($geometryColumn)) { |
183
|
4 |
|
$geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : ''; |
184
|
4 |
|
$query->whereRaw("ST_Distance({$geometryColumn}, ST_GeomFromText(?)) <= ?", [ |
185
|
4 |
|
$this->toWkt($geometry), |
186
|
4 |
|
$distance, |
187
|
|
|
]); |
188
|
|
|
} |
189
|
|
|
|
190
|
4 |
|
return $query; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
195
|
|
|
* @param $geometryColumn |
196
|
|
|
* @param $geometry |
197
|
|
|
* @param $distance |
198
|
|
|
* |
199
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
200
|
|
|
*/ |
201
|
3 |
|
public function scopeDistanceExcludingSelf($query, $geometryColumn, $geometry, $distance): EloquentBuilder |
202
|
|
|
{ |
203
|
3 |
|
if ($this->isColumnAllowed($geometryColumn)) { |
204
|
3 |
|
$query = $this->scopeDistance($query, $geometryColumn, $geometry, $distance); |
205
|
|
|
|
206
|
3 |
|
$geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : ''; |
207
|
3 |
|
$query->whereRaw("ST_Distance({$geometryColumn}, ST_GeomFromText(?)) != 0", [ |
208
|
3 |
|
$this->toWkt($geometry), |
209
|
|
|
]); |
210
|
|
|
} |
211
|
|
|
|
212
|
3 |
|
return $query; |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
217
|
|
|
* @param $geometryColumn |
218
|
|
|
* @param $geometry |
219
|
|
|
* |
220
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
221
|
|
|
*/ |
222
|
4 |
|
public function scopeDistanceValue($query, $geometryColumn, $geometry): EloquentBuilder |
223
|
|
|
{ |
224
|
4 |
|
if ($this->isColumnAllowed($geometryColumn)) { |
225
|
4 |
|
$columns = $query->getQuery()->columns; |
226
|
|
|
|
227
|
4 |
|
if (!$columns) { |
|
|
|
|
228
|
3 |
|
$query->select('*'); |
229
|
|
|
} |
230
|
|
|
|
231
|
4 |
|
$geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : ''; |
232
|
4 |
|
$query->selectRaw("ST_Distance({$geometryColumn}, ST_GeomFromText(?)) as distance", [ |
233
|
4 |
|
$this->toWkt($geometry), |
234
|
|
|
]); |
235
|
|
|
} |
236
|
|
|
|
237
|
4 |
|
return $query; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
242
|
|
|
* @param $geometryColumn |
243
|
|
|
* @param $geometry |
244
|
|
|
* @param $distance |
245
|
|
|
* |
246
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
247
|
|
|
*/ |
248
|
4 |
|
public function scopeDistanceSphere($query, $geometryColumn, $geometry, $distance): EloquentBuilder |
249
|
|
|
{ |
250
|
4 |
|
$distFunc = $this->getConnection() instanceof PostgresConnection ? 'ST_DistanceSphere' : 'ST_Distance_Sphere'; |
251
|
|
|
|
252
|
4 |
|
if ($this->isColumnAllowed($geometryColumn)) { |
253
|
4 |
|
$geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : ''; |
254
|
4 |
|
$query->whereRaw("{$distFunc}({$geometryColumn}, ST_GeomFromText(?)) <= ?", [ |
255
|
4 |
|
$this->toWkt($geometry), |
256
|
4 |
|
$distance, |
257
|
|
|
]); |
258
|
|
|
} |
259
|
|
|
|
260
|
4 |
|
return $query; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
265
|
|
|
* @param $geometryColumn |
266
|
|
|
* @param $geometry |
267
|
|
|
* @param $distance |
268
|
|
|
* |
269
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
270
|
|
|
*/ |
271
|
3 |
|
public function scopeDistanceSphereExcludingSelf($query, $geometryColumn, $geometry, $distance): EloquentBuilder |
272
|
|
|
{ |
273
|
3 |
|
$distFunc = $this->getConnection() instanceof PostgresConnection ? 'ST_DistanceSphere' : 'ST_Distance_Sphere'; |
274
|
|
|
|
275
|
3 |
|
if ($this->isColumnAllowed($geometryColumn)) { |
276
|
3 |
|
$query = $this->scopeDistanceSphere($query, $geometryColumn, $geometry, $distance); |
277
|
|
|
|
278
|
3 |
|
$geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : ''; |
279
|
3 |
|
$query->whereRaw("{$distFunc}({$geometryColumn}, ST_GeomFromText(?)) != 0", [ |
280
|
3 |
|
$this->toWkt($geometry), |
281
|
|
|
]); |
282
|
|
|
} |
283
|
|
|
|
284
|
3 |
|
return $query; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
289
|
|
|
* @param $geometryColumn |
290
|
|
|
* @param $geometry |
291
|
|
|
* |
292
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
293
|
|
|
*/ |
294
|
4 |
|
public function scopeDistanceSphereValue($query, $geometryColumn, $geometry): EloquentBuilder |
295
|
|
|
{ |
296
|
4 |
|
$distFunc = $this->getConnection() instanceof PostgresConnection ? 'ST_DistanceSphere' : 'ST_Distance_Sphere'; |
297
|
|
|
|
298
|
4 |
|
if ($this->isColumnAllowed($geometryColumn)) { |
299
|
4 |
|
$columns = $query->getQuery()->columns; |
300
|
|
|
|
301
|
4 |
|
if (!$columns) { |
|
|
|
|
302
|
3 |
|
$query->select('*'); |
303
|
|
|
} |
304
|
|
|
|
305
|
4 |
|
$geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : ''; |
306
|
4 |
|
$query->selectRaw("{$distFunc}({$geometryColumn}, ST_GeomFromText(?)) as distance", [ |
307
|
4 |
|
$this->toWkt($geometry), |
308
|
|
|
]); |
309
|
|
|
} |
310
|
|
|
|
311
|
4 |
|
return $query; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
316
|
|
|
* @param $geometryColumn |
317
|
|
|
* @param $geometry |
318
|
|
|
* @param $relationship |
319
|
|
|
* |
320
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
321
|
|
|
*/ |
322
|
9 |
|
public function scopeComparison($query, $geometryColumn, $geometry, $relationship): EloquentBuilder |
323
|
|
|
{ |
324
|
9 |
|
if ($this->isColumnAllowed($geometryColumn)) { |
325
|
9 |
|
$relationship = ucfirst(strtolower($relationship)); |
326
|
|
|
|
327
|
9 |
|
if (!in_array($relationship, $this->stRelations, true)) { |
328
|
|
|
throw new UnknownSpatialRelationFunction($relationship); |
329
|
|
|
} |
330
|
|
|
|
331
|
9 |
|
$geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : ''; |
332
|
9 |
|
$query->whereRaw("ST_{$relationship}(`{$geometryColumn}`, ST_GeomFromText(?))", [ |
333
|
9 |
|
$this->toWkt($geometry), |
334
|
|
|
]); |
335
|
|
|
} |
336
|
|
|
|
337
|
9 |
|
return $query; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
342
|
|
|
* @param $geometryColumn |
343
|
|
|
* @param $polygon |
344
|
|
|
* |
345
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
346
|
|
|
*/ |
347
|
1 |
|
public function scopeWithin($query, $geometryColumn, $polygon): EloquentBuilder |
348
|
|
|
{ |
349
|
1 |
|
return $this->scopeComparison($query, $geometryColumn, $polygon, 'within'); |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
/** |
353
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
354
|
|
|
* @param $geometryColumn |
355
|
|
|
* @param $geometry |
356
|
|
|
* |
357
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
358
|
|
|
*/ |
359
|
1 |
|
public function scopeCrosses($query, $geometryColumn, $geometry): EloquentBuilder |
360
|
|
|
{ |
361
|
1 |
|
return $this->scopeComparison($query, $geometryColumn, $geometry, 'crosses'); |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
/** |
365
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
366
|
|
|
* @param $geometryColumn |
367
|
|
|
* @param $geometry |
368
|
|
|
* |
369
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
370
|
|
|
*/ |
371
|
1 |
|
public function scopeContains($query, $geometryColumn, $geometry): EloquentBuilder |
372
|
|
|
{ |
373
|
1 |
|
return $this->scopeComparison($query, $geometryColumn, $geometry, 'contains'); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
378
|
|
|
* @param $geometryColumn |
379
|
|
|
* @param $geometry |
380
|
|
|
* |
381
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
382
|
|
|
*/ |
383
|
1 |
|
public function scopeDisjoint($query, $geometryColumn, $geometry): EloquentBuilder |
384
|
|
|
{ |
385
|
1 |
|
return $this->scopeComparison($query, $geometryColumn, $geometry, 'disjoint'); |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
/** |
389
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
390
|
|
|
* @param $geometryColumn |
391
|
|
|
* @param $geometry |
392
|
|
|
* |
393
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
394
|
|
|
*/ |
395
|
1 |
|
public function scopeEquals($query, $geometryColumn, $geometry): EloquentBuilder |
396
|
|
|
{ |
397
|
1 |
|
return $this->scopeComparison($query, $geometryColumn, $geometry, 'equals'); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
/** |
401
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
402
|
|
|
* @param $geometryColumn |
403
|
|
|
* @param $geometry |
404
|
|
|
* |
405
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
406
|
|
|
*/ |
407
|
1 |
|
public function scopeIntersects($query, $geometryColumn, $geometry): EloquentBuilder |
408
|
|
|
{ |
409
|
1 |
|
return $this->scopeComparison($query, $geometryColumn, $geometry, 'intersects'); |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
/** |
413
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
414
|
|
|
* @param $geometryColumn |
415
|
|
|
* @param $geometry |
416
|
|
|
* |
417
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
418
|
|
|
*/ |
419
|
1 |
|
public function scopeOverlaps($query, $geometryColumn, $geometry): EloquentBuilder |
420
|
|
|
{ |
421
|
1 |
|
return $this->scopeComparison($query, $geometryColumn, $geometry, 'overlaps'); |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
/** |
425
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
426
|
|
|
* @param $geometryColumn |
427
|
|
|
* @param $geometry |
428
|
|
|
* |
429
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
430
|
|
|
*/ |
431
|
1 |
|
public function scopeDoesTouch($query, $geometryColumn, $geometry): EloquentBuilder |
432
|
|
|
{ |
433
|
1 |
|
return $this->scopeComparison($query, $geometryColumn, $geometry, 'touches'); |
434
|
|
|
} |
435
|
|
|
} |
436
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.