Passed
Pull Request — master (#23)
by
unknown
05:07
created

GIS::reproject_array()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 3
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
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 {
115
                    return null;
116
                }
117
            default: throw new Exception('Unkown property ' . $property);
118
        }
119
    }
120
121 19
    public function __toString()
122
    {
123 19
        if (is_string($this->value)) {
124 11
            return $this->value;
125
        }
126
127 13
        $type = isset($this->value['type']) ? strtoupper($this->value['type']) : null;
128 13
        $srid = isset($this->value['srid']) ? $this->value['srid'] : GIS::config()->default_srid;
129 13
        $array = isset($this->value['coordinates']) ? $this->value['coordinates'] : $this->value;
130
131
        $replacements = [
132 13
            '/(?<=\d),(?=-|\d)/' => ' ',
133
            '/\[\[\[\[/' => '(((',
134
            '/\]\]\]\]/' => ')))',
135
            '/\[\[\[/' => '((',
136
            '/\]\]\]/' => '))',
137
            '/\[\[/' => '(',
138
            '/\]\]/' => ')',
139
            '/\[/' => '',
140
            '/\]/' => '',
141
        ];
142
143 13
        $coords = preg_replace(array_keys($replacements), array_values($replacements), json_encode($array));
144
145 13
        return sprintf('SRID=%d;%s%s', $srid, $type, $type == 'POINT' ? "($coords)" : $coords);
146
    }
147
148 9
    public function isNull()
149
    {
150 9
        return empty($this->value) || isset($this->value['coordinates']) && empty($this->value['coordinates']);
151
    }
152
153 9
    public static function of($dataObjectClass)
154
    {
155 9
        if ($field = $dataObjectClass::config()->get('default_geo_field')) {
156
            return $field;
157
        }
158
159 9
        foreach ($dataObjectClass::config()->get('db') ?: [] as $field => $type) {
160 8
            if ($type == 'Geography' || $type == 'Geometry') {
161 8
                return $field;
162
            }
163
        }
164 1
    }
165
166
    /**
167
     * reproject an array representation of a geometry to the given srid
168
     */
169 12
    public function reproject($toSrid = 4326)
170
    {
171 12
        $fromSrid = $this->srid;
172 12
        $fromCoordinates = $this->coordinates;
173 12
        $type = $this->type;
174
175 12
        if ($fromSrid != $toSrid) {
176 1
            $fromProj = self::get_proj4($fromSrid);
177 1
            $toProj = self::get_proj4($toSrid);
178 1
            $toCoordinates = self::reproject_array($fromCoordinates, $fromProj, $toProj);
179
        } else {
180 11
            $toCoordinates = $fromCoordinates;
181
        }
182
183 12
        return GIS::create([
184 12
            'srid' => $toSrid,
185 12
            'type' => $type,
186 12
            'coordinates' => $toCoordinates,
187
        ]);
188
    }
189
190
    /**
191
     * @var proj4php instance
192
     */
193
    protected static $proj4;
194
195 1
    protected static function get_proj4($srid)
196
    {
197 1
        self::$proj4 = self::$proj4 ?: new Proj4php();
198
199 1
        if (!self::$proj4->hasDef('EPSG:' . $srid)) {
200
201 1
            $projDefs = Config::inst()->get(self::class, 'projections');
202
203 1
            if (!isset($projDefs[$srid])) {
204
                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.");
205
            }
206
207 1
            self::$proj4->addDef('EPSG:' . $srid, $projDefs[$srid]);
208
        }
209
210 1
        return new Proj('EPSG:' . $srid, self::$proj4);
211
    }
212
213
    protected static function reproject_array($coordinates, $fromProj, $toProj)
214
    {
215 1
        return self::each($coordinates, function($coordinate) use ($fromProj, $toProj) {
216 1
            return array_slice(self::$proj4->transform($toProj, new Point($coordinate[0], $coordinate[1], $fromProj))->toArray(), 0, 2);
217 1
        });
218
    }
219
220 5
    public static function each($coordinates, $callback)
221
    {
222 5
        if ($coordinates instanceof GIS) {
223 4
            $coordinates = $coordinates->coordinates;
224
        }
225
226 5
        if (is_array($coordinates[0])) {
227
228 2
            foreach ($coordinates as &$coordinate) {
229 2
                $coordinate = self::each($coordinate, $callback);
230
            }
231
232 2
            return $coordinates;
233
        }
234
235 5
        return $callback($coordinates);
236
    }
237
238 1
    public function distance($geo)
239
    {
240 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

240
        return DB::query('select ' . DB::get_schema()->/** @scrutinizer ignore-call */ translateDistanceQuery($this, GIS::create($geo)))->value();
Loading history...
241
    }
242
}
243