Passed
Push — master ( dd9083...a468b8 )
by Andreas
07:10
created

GIS   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 188
Duplicated Lines 0 %

Test Coverage

Coverage 87.21%

Importance

Changes 0
Metric Value
eloc 99
dl 0
loc 188
ccs 75
cts 86
cp 0.8721
rs 8.8
c 0
b 0
f 0
wmc 45

10 Methods

Rating   Name   Duplication   Size   Complexity  
A ewkt_to_array() 0 12 3
A get_proj4() 0 13 4
A of() 0 9 6
A reproject_array() 0 24 6
A reproject() 0 4 1
C to_ewkt() 0 34 12
A split_ewkt() 0 12 3
A distance() 0 3 1
A get_type() 0 13 5
A each() 0 14 4

How to fix   Complexity   

Complex Class

Complex classes like GIS often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GIS, and based on these observations, apply Extract Interface, too.

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 proj4php\Proj4php;
11
use proj4php\Proj;
12
use proj4php\Point;
13
use Exception;
14
15
class GIS
16
{
17
    use Configurable;
18
19
    use Injectable;
20
21
    private static $default_srid = 4326;
0 ignored issues
show
introduced by
The private property $default_srid is not used, and could be removed.
Loading history...
22
23
    const EWKT_PATTERN = '/^SRID=(\d+);(([A-Z]+)\s*(\(.+\)))$/i';
24
25
    const TYPES = [
26
        'point' => 'Point',
27
        'linestring' => 'LineString',
28
        'polygon' => 'Polygon',
29
        'multipolygon' => 'MultiPolygon',
30
    ];
31
32 8
    public static function of($dataObjectClass)
33
    {
34 8
        if ($field = $dataObjectClass::config()->get('default_geo_field')) {
35
            return $field;
36
        }
37
38 8
        foreach ($dataObjectClass::config()->get('db') ?: [] as $field => $type) {
39 8
            if ($type == 'Geography' || $type == 'Geometry') {
40 8
                return $field;
41
            }
42
        }
43
    }
44
45 12
    public static function to_ewkt($array)
46
    {
47 12
        if (is_string($array)) return $array;
48
49 12
        if ($array instanceof DBGeography) $array = $array->getValue();
50
51 12
        $type = isset($array['type']) ? strtoupper($array['type']) : null;
52 12
        $srid = isset($array['srid']) ? $array['srid'] : Config::inst()->get(self::class, 'default_srid');
53 12
        $array = isset($array['coordinates']) ? $array['coordinates'] : $array;
54
55 12
        if (!$type) {
56
            switch (true) {
57 5
                case is_numeric($array[0]): $type = 'POINT'; break;
58 1
                case is_numeric($array[0][0]): $type = 'LINESTRING'; break;
59 1
                case is_numeric($array[0][0][0]): $type = 'POLYGON'; break;
60 1
                case is_numeric($array[0][0][0][0]): $type = 'MULTIPOLYGON'; break;
61
            }
62
        }
63
64
        $replacements = [
65 12
            '/(?<=\d),(?=-|\d)/' => ' ',
66
            '/\[\[\[\[/' => '(((',
67
            '/\]\]\]\]/' => ')))',
68
            '/\[\[\[/' => '((',
69
            '/\]\]\]/' => '))',
70
            '/\[\[/' => '(',
71
            '/\]\]/' => ')',
72
            '/\[/' => '',
73
            '/\]/' => '',
74
        ];
75
76 12
        $coords = preg_replace(array_keys($replacements), array_values($replacements), json_encode($array));
77
78 12
        return sprintf('SRID=%d;%s%s', $srid, $type, $type == 'POINT' ? "($coords)" : $coords);
79
    }
80
81 11
    public static function ewkt_to_array($ewkt)
82
    {
83 11
        if (preg_match(self::EWKT_PATTERN, $ewkt, $matches)) {
84 11
            $coords = str_replace(['(', ')'], ['[', ']'], preg_replace('/([\d\.-]+)\s+([\d\.-]+)/', "[$1,$2]", $matches[4]));
85 11
            if (strtolower($matches[3]) != 'point') {
86 6
                $coords = "[$coords]";
87
            }
88
89
            return [
90 11
                'srid' => $matches[1],
91 11
                'type' => self::TYPES[strtolower($matches[3])],
92 11
                'coordinates' => json_decode($coords, true)[0],
93
            ];
94
        }
95
    }
96
97 14
    public static function split_ewkt($ewkt, $fallbackSrid = null)
98
    {
99 14
        $fallbackSrid = $fallbackSrid ?: Config::inst()->get(self::class, 'default_srid');
100
101 14
        if (preg_match(GIS::EWKT_PATTERN, $ewkt, $matches)) {
102 14
            $wkt = $matches[2];
103 14
            $srid = (int)$matches[1];
104
        } else {
105
            $wkt = $ewkt;
106
            $srid = (int)$fallbackSrid;
107
        }
108 14
        return [$wkt, $srid];
109
    }
110
111 5
    public static function get_type($geometry, $useBestGuess = false)
112
    {
113 5
        if (is_array($geometry) && isset($geometry['type'])) {
114
            return self::TYPES[strtolower($geometry['type'])];
115 5
        } elseif (is_array($geometry)) {
116 1
            $geometry = self::to_ewkt($geometry, null, $useBestGuess);
0 ignored issues
show
Unused Code introduced by
The call to Smindel\GIS\GIS::to_ewkt() has too many arguments starting with null. ( Ignorable by Annotation )

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

116
            /** @scrutinizer ignore-call */ 
117
            $geometry = self::to_ewkt($geometry, null, $useBestGuess);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
117
        }
118 5
        if (preg_match(
119 5
            '/;(' . implode('|', array_keys(self::TYPES)) . ')\(/i',
120 5
            strtolower(substr($geometry, 8, 30)),
121 5
            $matches
122
        )) {
123 5
            return self::TYPES[$matches[1]];
124
        }
125
    }
126
127
    /**
128
     * reproject an array representation of a geometry to the given srid
129
     */
130 7
    public static function reproject_array($array, $toSrid = 4326)
131
    {
132 7
        if (isset($array['srid'])) {
133 7
            $fromSrid = $array['srid'];
134 7
            $fromCoordinates = $array['coordinates'];
135 7
            $type = $array['type'];
136
        } else {
137
            $fromSrid = Config::inst()->get(self::class, 'default_srid') ?: 4326;
138
            $fromCoordinates = $array;
139
            $type = is_array($array[0]) ? (is_array($array[0][0]) ? 'Polygon' : 'LineString') : 'Point';
140
        }
141
142 7
        if ($fromSrid != $toSrid) {
143 1
            $fromProj = self::get_proj4($fromSrid);
144 1
            $toProj = self::get_proj4($toSrid);
145 1
            $toCoordinates = self::reproject($fromCoordinates, $fromProj, $toProj);
146
        } else {
147 6
            $toCoordinates = $fromCoordinates;
148
        }
149
150
        return [
151 7
            'srid' => $toSrid,
152 7
            'type' => $type,
153 7
            'coordinates' => $toCoordinates,
154
        ];
155
    }
156
157
    /**
158
     * @var proj4php instance
159
     */
160
    protected static $proj4;
161
162 1
    protected static function get_proj4($srid)
163
    {
164 1
        self::$proj4 = self::$proj4 ?: new Proj4php();
165
166 1
        if (!self::$proj4->hasDef('EPSG:' . $srid)) {
167 1
            $projDefs = Config::inst()->get(self::class, 'projections');
168 1
            if (!isset($projDefs[$srid])) {
169
                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.");
170
            }
171 1
            self::$proj4->addDef('EPSG:' . $srid, $projDefs[$srid]);
172
        }
173
174 1
        return new Proj('EPSG:' . $srid, self::$proj4);
175
    }
176
177
    protected static function reproject($coordinates, $fromProj, $toProj)
178
    {
179 1
        return self::each($coordinates, function($coordinate) use ($fromProj, $toProj) {
180 1
            return array_slice(self::$proj4->transform($toProj, new Point($coordinate[0], $coordinate[1], $fromProj))->toArray(), 0, 2);
181 1
        });
182
    }
183
184 5
    public static function each($coordinates, $callback)
185
    {
186 5
        if (isset($coordinates['coordinates'])) {
187 4
            $coordinates = $coordinates['coordinates'];
188
        }
189
190 5
        if (is_array($coordinates[0])) {
191 2
            foreach ($coordinates as &$coordinate) {
192 2
                $coordinate = self::each($coordinate, $callback);
193
            }
194 2
            return $coordinates;
195
        }
196
197 5
        return $callback($coordinates);
198
    }
199
200 1
    public static function distance($geo1, $geo2)
201
    {
202 1
        return DB::query('select ' . DB::get_schema()->translateDistanceQuery($geo1, $geo2))->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

202
        return DB::query('select ' . DB::get_schema()->/** @scrutinizer ignore-call */ translateDistanceQuery($geo1, $geo2))->value();
Loading history...
203
    }
204
}
205