Passed
Push — master ( 6d7a51...05f3bf )
by Andreas
05:30 queued 30s
created

GIS::__toString()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 9
nop 0
dl 0
loc 23
ccs 8
cts 8
cp 1
crap 6
rs 9.1111
c 0
b 0
f 0
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
    ];
41
42
    private static $default_srid = 4326;
43
44
    protected $value;
45
46
    /**
47
     * Constructor
48
     *
49
     * @param $value mixed geo value:
50
     *      string: wkt (default srid) or ewkt
51
     *      array: just coordinates (default srid, autodetect shape) or assoc with type, srid and coordinates
52
     *      GIS: extract value
53
     *      DBGeography: extract value
54
     */
55 28
    public function __construct($value)
56
    {
57 28
        if ($value instanceof GIS) {
58 3
            $this->value = $value->value;
59 28
        } else if ($value instanceof DBGeography) {
60
            $this->value = $value->getValue();
61 28
        } else if (is_string($value) && preg_match(self::WKT_PATTERN, $value)) {
62
            $this->value = 'SRID=' . self::config()->default_srid . ';' . $value;
63
        } else if (
64 28
            (is_array($value) && count($value) == 3 && isset($value['type']))
65 25
            || (is_string($value) && preg_match(self::EWKT_PATTERN, $value))
66
        ) {
67 26
            $this->value = $value;
68 8
        } else if (empty($value) || isset($value['coordinates']) && empty($value['coordinates'])) {
69
            $this->value = null;
70 8
        } else if (is_array($value) && !isset($value['type'])) {
71
            switch (true) {
72 8
                case is_numeric($value[0]): $type = 'Point'; break;
73 1
                case is_numeric($value[0][0]): $type = 'LineString'; break;
74 1
                case is_numeric($value[0][0][0]): $type = 'Polygon'; break;
75 1
                case is_numeric($value[0][0][0][0]): $type = 'MultiPolygon'; break;
76
            }
77 8
            $this->value = [
78 8
                'srid' => GIS::config()->default_srid,
79 8
                '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...
80 8
                'coordinates' => $value,
81
            ];
82
        } else {
83
            throw new Exception('Invalid geo value');
84
        }
85 28
    }
86
87
    public function __isset($property)
88
    {
89
        return array_search($property, ['array', 'ewkt', 'srid', 'type', 'coordinates']) !== false;
90
    }
91
92 28
    public function __get($property)
93
    {
94 28
        if (isset($this->value[$property])) {
95 13
            return $this->value[$property];
96
        }
97
98
        switch ($property) {
99 26
            case 'array': return ['srid' => $this->srid, 'type' => $this->type, 'coordinates' => $this->coordinates];
100 26
            case 'ewkt': return (string)$this;
101 23
            case 'wkt': return explode(';', (string)$this)[1];
102 22
            case 'srid': return preg_match('/^SRID=(\d+);/i', $this->value, $matches) ? (int)$matches[1] : null;
103 16
            case 'type': return preg_match('/^SRID=\d+;(' . implode('|', array_change_key_case(self::TYPES, CASE_UPPER)) . ')/i', $this->value, $matches) ? self::TYPES[strtolower($matches[1])] : null;
104 15
            case 'coordinates':
105 15
                if (preg_match(self::EWKT_PATTERN, $this->value, $matches)) {
106
107 15
                    $coords = str_replace(['(', ')'], ['[', ']'], preg_replace('/([\d\.-]+)\s+([\d\.-]+)/', "[$1,$2]", $matches[4]));
108
109 15
                    if (strtolower($matches[3]) != 'point') {
110 9
                        $coords = "[$coords]";
111
                    }
112
113 15
                    return json_decode($coords, true)[0];
114
                } else return null;
115
            default: throw new Exception('Unkown property ' . $property);
116
        }
117
    }
118
119 19
    public function __toString()
120
    {
121 19
        if (is_string($this->value)) return $this->value;
122
123 13
        $type = isset($this->value['type']) ? strtoupper($this->value['type']) : null;
124 13
        $srid = isset($this->value['srid']) ? $this->value['srid'] : GIS::config()->default_srid;
125 13
        $array = isset($this->value['coordinates']) ? $this->value['coordinates'] : $this->value;
126
127
        $replacements = [
128 13
            '/(?<=\d),(?=-|\d)/' => ' ',
129
            '/\[\[\[\[/' => '(((',
130
            '/\]\]\]\]/' => ')))',
131
            '/\[\[\[/' => '((',
132
            '/\]\]\]/' => '))',
133
            '/\[\[/' => '(',
134
            '/\]\]/' => ')',
135
            '/\[/' => '',
136
            '/\]/' => '',
137
        ];
138
139 13
        $coords = preg_replace(array_keys($replacements), array_values($replacements), json_encode($array));
140
141 13
        return sprintf('SRID=%d;%s%s', $srid, $type, $type == 'POINT' ? "($coords)" : $coords);
142
    }
143
144 9
    public function isNull()
145
    {
146 9
        return empty($this->value) || isset($this->value['coordinates']) && empty($this->value['coordinates']);
147
    }
148
149 9
    public static function of($dataObjectClass)
150
    {
151 9
        if ($field = $dataObjectClass::config()->get('default_geo_field')) {
152
            return $field;
153
        }
154
155 9
        foreach ($dataObjectClass::config()->get('db') ?: [] as $field => $type) {
156 8
            if ($type == 'Geography' || $type == 'Geometry') {
157 8
                return $field;
158
            }
159
        }
160 1
    }
161
162
    /**
163
     * reproject an array representation of a geometry to the given srid
164
     */
165 12
    public function reproject($toSrid = 4326)
166
    {
167 12
        $fromSrid = $this->srid;
168 12
        $fromCoordinates = $this->coordinates;
169 12
        $type = $this->type;
170
171 12
        if ($fromSrid != $toSrid) {
172 1
            $fromProj = self::get_proj4($fromSrid);
173 1
            $toProj = self::get_proj4($toSrid);
174 1
            $toCoordinates = self::reproject_array($fromCoordinates, $fromProj, $toProj);
175
        } else {
176 11
            $toCoordinates = $fromCoordinates;
177
        }
178
179 12
        return GIS::create([
180 12
            'srid' => $toSrid,
181 12
            'type' => $type,
182 12
            'coordinates' => $toCoordinates,
183
        ]);
184
    }
185
186
    /**
187
     * @var proj4php instance
188
     */
189
    protected static $proj4;
190
191 1
    protected static function get_proj4($srid)
192
    {
193 1
        self::$proj4 = self::$proj4 ?: new Proj4php();
194
195 1
        if (!self::$proj4->hasDef('EPSG:' . $srid)) {
196
197 1
            $projDefs = Config::inst()->get(self::class, 'projections');
198
199 1
            if (!isset($projDefs[$srid])) {
200
                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.");
201
            }
202
203 1
            self::$proj4->addDef('EPSG:' . $srid, $projDefs[$srid]);
204
        }
205
206 1
        return new Proj('EPSG:' . $srid, self::$proj4);
207
    }
208
209
    protected static function reproject_array($coordinates, $fromProj, $toProj)
210
    {
211 1
        return self::each($coordinates, function($coordinate) use ($fromProj, $toProj) {
212 1
            return array_slice(self::$proj4->transform($toProj, new Point($coordinate[0], $coordinate[1], $fromProj))->toArray(), 0, 2);
213 1
        });
214
    }
215
216 5
    public static function each($coordinates, $callback)
217
    {
218 5
        if ($coordinates instanceof GIS) {
219 4
            $coordinates = $coordinates->coordinates;
220
        }
221
222 5
        if (is_array($coordinates[0])) {
223
224 2
            foreach ($coordinates as &$coordinate) {
225 2
                $coordinate = self::each($coordinate, $callback);
226
            }
227
228 2
            return $coordinates;
229
        }
230
231 5
        return $callback($coordinates);
232
    }
233
234 1
    public function distance($geo)
235
    {
236 1
        return DB::query('select ' . DB::get_schema()->translateDistanceQuery($this, GIS::create($geo)))->value();
0 ignored issues
show
Bug introduced by
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

236
        return DB::query('select ' . DB::get_schema()->/** @scrutinizer ignore-call */ translateDistanceQuery($this, GIS::create($geo)))->value();
Loading history...
237
    }
238
}
239