Issues (52)

src/GIS.php (4 issues)

1
<?php
2
3
namespace Smindel\GIS;
4
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\Core\Config\Configurable;
7
use SilverStripe\Core\Injector\Injectable;
8
use SilverStripe\ORM\DB;
9
use Smindel\GIS\ORM\FieldType\DBGeography;
10
use Smindel\GIS\ORM\FieldType\DBGeometry;
11
use proj4php\Proj4php;
12
use proj4php\Proj;
13
use proj4php\Point;
14
use Exception;
15
16
/**
17
 * @property string $array
18
 * @property string $ewkt
19
 * @property string $wkt
20
 * @property string $srid
21
 * @property string $type
22
 * @property string $coordinates
23
 */
24
class GIS
25
{
26
    use Configurable;
27
28
    use Injectable;
29
30
    const WKT_PATTERN = '/^(([A-Z]+)\s*(\(.+\)))$/i';
31
    const EWKT_PATTERN = '/^SRID=(\d+);(([A-Z]+)\s*(\(.+\)))$/i';
32
33
    const TYPES = [
34
        'point' => 'Point',
35
        'linestring' => 'LineString',
36
        'polygon' => 'Polygon',
37
        'multipoint' => 'MultiPoint',
38
        'multilinestring' => 'MultiLineString',
39
        'multipolygon' => 'MultiPolygon',
40
        'geometrycollection' => 'GeometryCollection'
41
    ];
42
43
    private static $default_srid = 4326;
44
45
    protected $value;
46
47
    /**
48
     * Constructor
49
     *
50
     * @param $value mixed geo value:
51
     *      string: wkt (default srid) or ewkt
52
     *      array: just coordinates (default srid, autodetect shape) or assoc with type, srid and coordinates
53
     *      GIS: extract value
54
     *      DBGeography: extract value
55 28
     */
56
    public function __construct($value)
57 28
    {
58 3
        DB::get_schema()->initialise();
0 ignored issues
show
The method initialise() does not exist on SilverStripe\ORM\Connect\DBSchemaManager. It seems like you code against a sub-type of SilverStripe\ORM\Connect\DBSchemaManager such as Smindel\GIS\ORM\MySQLGISSchemaManager. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

58
        DB::get_schema()->/** @scrutinizer ignore-call */ initialise();
Loading history...
59 28
        if ($value instanceof GIS) {
60
            $this->value = $value->value;
61 28
        } else if ($value instanceof DBGeography) {
62
            $this->value = $value->getValue();
63
        } else if (is_string($value) && preg_match(self::WKT_PATTERN, $value)) {
64 28
            $this->value = 'SRID=' . self::config()->default_srid . ';' . $value;
65 25
        } else if (is_array($value) && count($value) == 3 && isset($value['type'])) {
66
            $this->value = $value;
67 26
        } else if (is_string($value) && preg_match(self::EWKT_PATTERN, $value)) {
68 8
            $this->value = $value;
69
        } else if (empty($value) || isset($value['coordinates']) && empty($value['coordinates'])) {
70 8
            $this->value = null;
71
        } else if (is_array($value) && !isset($value['type'])) {
72 8
            switch (true) {
73 1
                case is_numeric($value[0]):
74 1
                    $type = 'Point';
75 1
                    break;
76
                case is_numeric($value[0][0]):
77 8
                    $type = 'LineString';
78 8
                    break;
79 8
                case is_numeric($value[0][0][0]):
80 8
                    $type = 'Polygon';
81
                    break;
82
                case is_numeric($value[0][0][0][0]):
83
                    $type = 'MultiPolygon';
84
                    break;
85 28
            }
86
            $this->value = [
87
                'srid' => GIS::config()->default_srid,
88
                'type' => $type,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $type does not seem to be defined for all execution paths leading up to this point.
Loading history...
89
                'coordinates' => $value,
90
            ];
91
        } else {
92 28
            throw new Exception('Invalid geo value: "' . var_export($value) . '"');
0 ignored issues
show
Are you sure the usage of var_export($value) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
93
        }
94 28
    }
95 13
96
    public function __isset($property)
97
    {
98
        return array_search($property, ['array', 'ewkt', 'srid', 'type', 'coordinates']) !== false;
99 26
    }
100 26
101 23
    public function __get($property)
102 22
    {
103 16
        if (isset($this->value[$property])) {
104 15
            return $this->value[$property];
105 15
        }
106
107 15
        switch ($property) {
108
            case 'array':
109 15
                return ['srid' => $this->srid, 'type' => $this->type, 'coordinates' => $this->coordinates];
110 9
            case 'ewkt':
111
                return (string)$this;
112
            case 'wkt':
113 15
                return explode(';', (string)$this)[1];
114
            case 'srid':
115
                return preg_match('/^SRID=(\d+);/i', $this->value, $matches) ? (int)$matches[1] : null;
116
            case 'type':
117
                return preg_match('/^SRID=\d+;(' . implode('|', array_change_key_case(self::TYPES, CASE_UPPER)) . ')/i', $this->value, $matches) ? self::TYPES[strtolower($matches[1])] : null;
118
            case 'coordinates':
119
                if (preg_match(self::EWKT_PATTERN, $this->value, $matches)) {
120
121 19
                    $coords = str_replace(['(', ')'], ['[', ']'], preg_replace('/([\d\.-]+)\s+([\d\.-]+)/', "[$1,$2]", $matches[4]));
122
123 19
                    if (strtolower($matches[3]) != 'point') {
124 11
                        $coords = "[$coords]";
125
                    }
126
127 13
                    return json_decode($coords, true)[0];
128 13
                } else {
129 13
                    return null;
130
                }
131
            case 'geometries':
132 13
                // primarily used for GeometryCollections
133
                // @todo: what's supposed to be returned for non-GeometryCollections?
134
                if (preg_match(self::EWKT_PATTERN, $this->value, $matches)) {
135
                    $geometries = preg_split('/,(?=[a-zA-Z])/', substr($matches[4], 1, -1));
136
                    $srid = $this->srid;
137
                    return array_map(function ($geometry) use ($srid) {
138
                        return GIS::create('SRID=' . $srid . ';' . $geometry);
139
                    }, $geometries);
140
                }
141
                return null;
142
            default:
143 13
                throw new Exception('Unkown property ' . $property);
144
        }
145 13
    }
146
147
    public function __toString()
148 9
    {
149
        if (is_string($this->value)) {
150 9
            return $this->value;
151
        }
152
153 9
        $type = isset($this->value['type']) ? strtoupper($this->value['type']) : null;
154
        $srid = isset($this->value['srid']) ? $this->value['srid'] : GIS::config()->default_srid;
155 9
        $array = isset($this->value['coordinates']) ? $this->value['coordinates'] : $this->value;
156
157
        $replacements = [
158
            '/(?<=\d),(?=-|\d)/' => ' ',
159 9
            '/\[\[\[\[/' => '(((',
160 8
            '/\]\]\]\]/' => ')))',
161 8
            '/\[\[\[/' => '((',
162
            '/\]\]\]/' => '))',
163
            '/\[\[/' => '(',
164 1
            '/\]\]/' => ')',
165
            '/\[/' => '',
166
            '/\]/' => '',
167
        ];
168
169 12
        $coords = preg_replace(array_keys($replacements), array_values($replacements), json_encode($array));
170
171 12
        return sprintf('SRID=%d;%s%s', $srid, $type, $type == 'POINT' ? "($coords)" : $coords);
172 12
    }
173 12
174
    public function isNull()
175 12
    {
176 1
        return empty($this->value) || isset($this->value['coordinates']) && empty($this->value['coordinates']);
177 1
    }
178 1
179
    public static function of($dataObjectClass)
180 11
    {
181
        if ($field = $dataObjectClass::config()->get('default_geo_field')) {
182
            return $field;
183 12
        }
184 12
185 12
        foreach ($dataObjectClass::config()->get('db') ?: [] as $field => $type) {
186 12
            if (in_array($type, ['Geography', 'Geometry', DBGeography::class, DBGeometry::class])) {
187
                return $field;
188
            }
189
        }
190
    }
191
192
    /**
193
     * reproject an array representation of a geometry to the given srid
194
     */
195 1
    public function reproject($toSrid = 4326)
196
    {
197 1
        $fromSrid = $this->srid;
198
199 1
        if ($fromSrid == $toSrid) {
200
            return clone $this;
201 1
        }
202
203 1
        $fromCoordinates = $this->coordinates;
204
        $type = $this->type;
205
206
        $fromProj = self::get_proj4($fromSrid);
207 1
        $toProj = self::get_proj4($toSrid);
208
        $toCoordinates = self::reproject_array($fromCoordinates, $fromProj, $toProj);
209
210 1
        return GIS::create([
211
            'srid' => $toSrid,
212
            'type' => $type,
213
            'coordinates' => $toCoordinates,
214
        ]);
215 1
    }
216 1
217 1
    /**
218
     * @var proj4php instance
219
     */
220 5
    protected static $proj4;
221
222 5
    protected static function get_proj4($srid)
223 4
    {
224
        self::$proj4 = self::$proj4 ?: new Proj4php();
225
226 5
        if (!self::$proj4->hasDef('EPSG:' . $srid)) {
227
228 2
            $projDefs = Config::inst()->get(self::class, 'projections');
229 2
230
            if (!isset($projDefs[$srid])) {
231
                throw new Exception("Cannot use unregistered SRID $srid. Register it's <a href=\"http://spatialreference.org/ref/epsg/$srid/proj4/\">PROJ.4 definition</a> in GIS::projections.");
232 2
            }
233
234
            self::$proj4->addDef('EPSG:' . $srid, $projDefs[$srid]);
235 5
        }
236
237
        return new Proj('EPSG:' . $srid, self::$proj4);
238 1
    }
239
240 1
    protected static function reproject_array($coordinates, $fromProj, $toProj)
241
    {
242
        return self::each($coordinates, function ($coordinate) use ($fromProj, $toProj) {
243
            return array_slice(self::$proj4->transform($toProj, new Point($coordinate[0], $coordinate[1], $fromProj))->toArray(), 0, 2);
244
        });
245
    }
246
247
    public static function each($coordinates, $callback)
248
    {
249
        if ($coordinates instanceof GIS) {
250
            $coordinates = $coordinates->coordinates;
251
        }
252
253
        if (is_array($coordinates[0])) {
254
255
            foreach ($coordinates as &$coordinate) {
256
                $coordinate = self::each($coordinate, $callback);
257
            }
258
259
            return $coordinates;
260
        }
261
262
        return $callback($coordinates);
263
    }
264
265
    public function distance($geo)
266
    {
267
        return DB::query('select ' . DB::get_schema()->translateDistanceQuery($this, GIS::create($geo)))->value();
0 ignored issues
show
The method translateDistanceQuery() does not exist on SilverStripe\ORM\Connect\DBSchemaManager. It seems like you code against a sub-type of SilverStripe\ORM\Connect\DBSchemaManager such as Smindel\GIS\ORM\MySQLGISSchemaManager. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

267
        return DB::query('select ' . DB::get_schema()->/** @scrutinizer ignore-call */ translateDistanceQuery($this, GIS::create($geo)))->value();
Loading history...
268
    }
269
}
270