Completed
Push — master ( 4d5f61...930dd2 )
by Michal
03:15
created

ShapeFile   D

Complexity

Total Complexity 102

Size/Duplication

Total Lines 497
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 66.43%

Importance

Changes 17
Bugs 0 Features 0
Metric Value
c 17
b 0
f 0
dl 0
loc 497
ccs 188
cts 283
cp 0.6643
rs 4.8717
wmc 102
lcom 1
cbo 2

30 Methods

Rating   Name   Duplication   Size   Complexity  
A supports_dbase() 0 4 1
A __construct() 0 6 1
B loadFromFile() 0 27 6
B saveToFile() 0 15 5
A _getFilename() 0 4 1
B updateBBox() 0 12 7
B addRecord() 0 22 5
A deleteRecord() 0 11 3
A getDBFHeader() 0 3 1
A setDBFHeader() 0 8 2
A getIndexFromDBFData() 0 11 4
B _loadDBFHeader() 0 34 6
A _deleteRecordFromDBF() 0 5 2
B _loadHeaders() 0 31 3
D _saveHeaders() 0 27 9
B _loadRecords() 0 17 6
D _saveRecords() 0 30 9
A _openFile() 0 10 3
A _openSHPFile() 0 7 2
A _closeSHPFile() 0 6 2
A _openSHXFile() 0 7 2
A _closeSHXFile() 0 6 2
A _createDBFFile() 0 14 3
C _openDBFFile() 0 23 9
A _closeDBFFile() 0 6 2
A setError() 0 3 1
A readSHP() 0 4 1
A eofSHP() 0 4 1
A getShapeName() 0 4 1
A hasMeasure() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like ShapeFile 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 ShapeFile, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * phpMyAdmin ShapeFile library
4
 * <https://github.com/phpmyadmin/shapefile/>
5
 *
6
 * Copyright 2006-2007 Ovidio <ovidio AT users.sourceforge.net>
7
 * Copyright 2016 Michal Čihař <[email protected]>
8
 *
9
 * This program is free software; you can redistribute it and/or
10
 * modify it under the terms of the GNU General Public License
11
 * as published by the Free Software Foundation.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with this program; if not, you can download one from
20
 * https://www.gnu.org/copyleft/gpl.html.
21
 */
22
namespace ShapeFile;
23
24
/**
25
 * ShapeFile class
26
 *
27
 * @package ShapeFile
28
 */
29
class ShapeFile {
30
    public $FileName;
31
32
    private $SHPFile = null;
33
    private $SHXFile = null;
34
    private $DBFFile = null;
35
36
    public $DBFHeader;
37
38
    public $lastError = '';
39
40
    public $boundingBox = array('xmin' => 0.0, 'ymin' => 0.0, 'xmax' => 0.0, 'ymax' => 0.0);
41
    private $fileLength = 0;
42
    public $shapeType = 0;
43
44
    public $records;
45
46
    /**
47
     * Checks whether dbase manipuations are supported.
48
     *
49
     * @return bool
50
     */
51 21
    public static function supports_dbase()
52
    {
53 21
        return extension_loaded('dbase');
54
    }
55
56
    /**
57
     * @param integer $shapeType
58
     */
59 24
    public function __construct($shapeType, $boundingBox = array('xmin' => 0.0, 'ymin' => 0.0, 'xmax' => 0.0, 'ymax' => 0.0), $FileName = null) {
60 24
        $this->shapeType = $shapeType;
61 24
        $this->boundingBox = $boundingBox;
62 24
        $this->FileName = $FileName;
63 24
        $this->fileLength = 50; // The value for file length is the total length of the file in 16-bit words (including the fifty 16-bit words that make up the header).
64 24
    }
65
66
    /**
67
     * @param string $FileName
68
     */
69 22
    public function loadFromFile($FileName) {
70 22
        if (!empty($FileName)) {
71 22
            $this->FileName = $FileName;
72 22
            $result = $this->_openSHPFile();
73 22
        } else {
74
            /* We operate on buffer emulated by readSHP / eofSHP */
75
            $result = true;
76
        }
77
78 22
        if ($result && ($this->_openDBFFile())) {
79 20
            if (!$this->_loadHeaders()) {
80 1
                $this->_closeSHPFile();
81 1
                $this->_closeDBFFile();
82 1
                return false;
83
            }
84 19
            if (!$this->_loadRecords()) {
85
                $this->_closeSHPFile();
86
                $this->_closeDBFFile();
87
                return false;
88
            }
89 19
            $this->_closeSHPFile();
90 19
            $this->_closeDBFFile();
91 19
            return true;
92
        } else {
93 2
            return false;
94
        }
95
    }
96
97
    /**
98
     * @param string|null $FileName Name of file to open
99
     */
100 13
    public function saveToFile($FileName = null) {
101 13
        if (!is_null($FileName)) {
102 13
            $this->FileName = $FileName;
103 13
        }
104
105 13
        if (($this->_openSHPFile(true)) && ($this->_openSHXFile(true)) && ($this->_openDBFFile(true))) {
106 13
            $this->_saveHeaders();
107 13
            $this->_saveRecords();
108 13
            $this->_closeSHPFile();
109 13
            $this->_closeSHXFile();
110 13
            $this->_closeDBFFile();
111 13
        } else {
112
            return false;
113
        }
114 13
    }
115
116
    /**
117
     * Generates filename with given extension
118
     *
119
     * @param string $extension Extension to use (including dot)
120
     *
121
     * @return string
122
     */
123 23
    private function _getFilename($extension)
124
    {
125 23
        return str_replace('.*', $extension, $this->FileName);
126
    }
127
128
    /**
129
     * Updates bounding box based on SHPData
130
     *
131
     * @param string $type Type of box
132
     * @param array  $data ShapeRecord SHPData
133
     *
134
     * @return void
135
     */
136 12
    private function updateBBox($type, $data)
137
    {
138 12
        $min = $type.'min';
139 12
        $max = $type.'max';
140
141 12
        if (!isset($this->boundingBox[$min]) || $this->boundingBox[$min] == 0.0 || ($this->boundingBox[$min] > $data[$min])) {
142 12
            $this->boundingBox[$min] = $data[$min];
143 12
        }
144 12
        if (!isset($this->boundingBox[$max]) || $this->boundingBox[$max] == 0.0 || ($this->boundingBox[$max] < $data[$max])) {
145 12
            $this->boundingBox[$max] = $data[$max];
146 12
        }
147 12
    }
148
149
    /**
150
     * @param ShapeRecord $record
151
     */
152 12
    public function addRecord($record) {
153 12
        if ((isset($this->DBFHeader)) && (is_array($this->DBFHeader))) {
154 12
            $record->updateDBFInfo($this->DBFHeader);
155 12
        }
156
157 12
        $this->fileLength += ($record->getContentLength() + 4);
158 12
        $this->records[] = $record;
159 12
        $this->records[count($this->records) - 1]->recordNumber = count($this->records);
160
161 12
        $this->updateBBox('x', $record->SHPData);
162 12
        $this->updateBBox('y', $record->SHPData);
163
164 12
        if (in_array($this->shapeType, array(11, 13, 15, 18, 21, 23, 25, 28))) {
165 8
            $this->updateBBox('m', $record->SHPData);
166 8
        }
167
168 12
        if (in_array($this->shapeType, array(11, 13, 15, 18))) {
169 4
            $this->updateBBox('z', $record->SHPData);
170 4
        }
171
172 12
        return (count($this->records) - 1);
173
    }
174
175
    /**
176
     * @param integer $index
177
     */
178
    public function deleteRecord($index) {
179
        if (isset($this->records[$index])) {
180
            $this->fileLength -= ($this->records[$index]->getContentLength() + 4);
181
            $count = count($this->records) - 1;
182
            for ($i = $index; $i < $count; $i++) {
183
                $this->records[$i] = $this->records[$i + 1];
184
            }
185
            unset($this->records[count($this->records) - 1]);
186
            $this->_deleteRecordFromDBF($index);
187
        }
188
    }
189
190
    public function getDBFHeader() {
191
        return $this->DBFHeader;
192
    }
193
194 12
    public function setDBFHeader($header) {
195 12
        $this->DBFHeader = $header;
196
197 12
        $count = count($this->records);
198 12
        for ($i = 0; $i < $count; $i++) {
199
            $this->records[$i]->updateDBFInfo($header);
200
        }
201 12
    }
202
203
    /**
204
     * Lookups value in the DBF file and returs index
205
     *
206
     * @param string $field Field to match
207
     * @param mixed  $value Value to match
208
     *
209
     * @return integer
210
     */
211 1
    public function getIndexFromDBFData($field, $value) {
212 1
        foreach ($this->records as $index => $record) {
213 1
            if (isset($record->DBFData[$field]) &&
214
                (trim(strtoupper($record->DBFData[$field])) == strtoupper($value))
215 1
            ) {
216
                return $index;
217
            }
218 1
        }
219
220 1
        return -1;
221
    }
222
223
    private function _loadDBFHeader() {
224
        $DBFFile = fopen($this->_getFilename('.dbf'), 'r');
225
226
        $result = array();
227
        $i = 1;
228
        $inHeader = true;
229
230
        while ($inHeader) {
231
            if (!feof($DBFFile)) {
232
                $buff32 = fread($DBFFile, 32);
233
                if ($i > 1) {
234
                    if (substr($buff32, 0, 1) == chr(13)) {
235
                        $inHeader = false;
236
                    } else {
237
                        $pos = strpos(substr($buff32, 0, 10), chr(0));
238
                        $pos = ($pos == 0 ? 10 : $pos);
239
240
                        $fieldName = substr($buff32, 0, $pos);
241
                        $fieldType = substr($buff32, 11, 1);
242
                        $fieldLen = ord(substr($buff32, 16, 1));
243
                        $fieldDec = ord(substr($buff32, 17, 1));
244
245
                        array_push($result, array($fieldName, $fieldType, $fieldLen, $fieldDec));
246
                    }
247
                }
248
                $i++;
249
            } else {
250
                $inHeader = false;
251
            }
252
        }
253
254
        fclose($DBFFile);
255
        return($result);
256
    }
257
258
    private function _deleteRecordFromDBF($index) {
259
        if (@dbase_delete_record($this->DBFFile, $index)) {
260
            dbase_pack($this->DBFFile);
261
        }
262
    }
263
264 20
    private function _loadHeaders() {
265 20
        if (Util::loadData('N', $this->readSHP(4)) != 0x270a) {
266 1
            $this->setError('Not a SHP file (file code mismatch)');
267 1
            return false;
268
        }
269
270
        /* Skip 20 unused bytes */
271 19
        $this->readSHP(20);
272
273 19
        $this->fileLength = Util::loadData('N', $this->readSHP(4));
274
275
        /* We currently ignore version */
276 19
        $this->readSHP(4);
277
278 19
        $this->shapeType = Util::loadData('V', $this->readSHP(4));
279
280 19
        $this->boundingBox = array();
281 19
        $this->boundingBox['xmin'] = Util::loadData('d', $this->readSHP(8));
282 19
        $this->boundingBox['ymin'] = Util::loadData('d', $this->readSHP(8));
283 19
        $this->boundingBox['xmax'] = Util::loadData('d', $this->readSHP(8));
284 19
        $this->boundingBox['ymax'] = Util::loadData('d', $this->readSHP(8));
285 19
        $this->boundingBox['zmin'] = Util::loadData('d', $this->readSHP(8));
286 19
        $this->boundingBox['zmax'] = Util::loadData('d', $this->readSHP(8));
287 19
        $this->boundingBox['mmin'] = Util::loadData('d', $this->readSHP(8));
288 19
        $this->boundingBox['mmax'] = Util::loadData('d', $this->readSHP(8));
289
290 19
        if (ShapeFile::supports_dbase()) {
291
            $this->DBFHeader = $this->_loadDBFHeader();
292
        }
293 19
        return true;
294
    }
295
296 13
    private function _saveHeaders() {
297 13
        fwrite($this->SHPFile, pack('NNNNNN', 9994, 0, 0, 0, 0, 0));
298 13
        fwrite($this->SHPFile, pack('N', $this->fileLength));
299 13
        fwrite($this->SHPFile, pack('V', 1000));
300 13
        fwrite($this->SHPFile, pack('V', $this->shapeType));
301 13
        fwrite($this->SHPFile, Util::packDouble($this->boundingBox['xmin']));
302 13
        fwrite($this->SHPFile, Util::packDouble($this->boundingBox['ymin']));
303 13
        fwrite($this->SHPFile, Util::packDouble($this->boundingBox['xmax']));
304 13
        fwrite($this->SHPFile, Util::packDouble($this->boundingBox['ymax']));
305 13
        fwrite($this->SHPFile, Util::packDouble(isset($this->boundingBox['zmin']) ? $this->boundingBox['zmin'] : 0));
306 13
        fwrite($this->SHPFile, Util::packDouble(isset($this->boundingBox['zmax']) ? $this->boundingBox['zmax'] : 0));
307 13
        fwrite($this->SHPFile, Util::packDouble(isset($this->boundingBox['mmin']) ? $this->boundingBox['mmin'] : 0));
308 13
        fwrite($this->SHPFile, Util::packDouble(isset($this->boundingBox['mmax']) ? $this->boundingBox['mmax'] : 0));
309
310 13
        fwrite($this->SHXFile, pack('NNNNNN', 9994, 0, 0, 0, 0, 0));
311 13
        fwrite($this->SHXFile, pack('N', 50 + 4 * count($this->records)));
312 13
        fwrite($this->SHXFile, pack('V', 1000));
313 13
        fwrite($this->SHXFile, pack('V', $this->shapeType));
314 13
        fwrite($this->SHXFile, Util::packDouble($this->boundingBox['xmin']));
315 13
        fwrite($this->SHXFile, Util::packDouble($this->boundingBox['ymin']));
316 13
        fwrite($this->SHXFile, Util::packDouble($this->boundingBox['xmax']));
317 13
        fwrite($this->SHXFile, Util::packDouble($this->boundingBox['ymax']));
318 13
        fwrite($this->SHXFile, Util::packDouble(isset($this->boundingBox['zmin']) ? $this->boundingBox['zmin'] : 0));
319 13
        fwrite($this->SHXFile, Util::packDouble(isset($this->boundingBox['zmax']) ? $this->boundingBox['zmax'] : 0));
320 13
        fwrite($this->SHXFile, Util::packDouble(isset($this->boundingBox['mmin']) ? $this->boundingBox['mmin'] : 0));
321 13
        fwrite($this->SHXFile, Util::packDouble(isset($this->boundingBox['mmax']) ? $this->boundingBox['mmax'] : 0));
322 13
    }
323
324 19
    private function _loadRecords() {
325
        /* Need to start at offset 100 */
326 19
        while (!$this->eofSHP()) {
327 19
            $record = new ShapeRecord(-1);
328 19
            $record->loadFromFile($this, $this->SHPFile, $this->DBFFile);
329 19
            if ($record->lastError != '') {
330
                $this->setError($record->lastError);
331
                return false;
332
            }
333 19
            if (($record->shapeType === false || $record->shapeType === '') && $this->eofSHP()) {
334 19
                break;
335
            }
336
337 19
            $this->records[] = $record;
338 19
        }
339 19
        return true;
340
    }
341
342 13
    private function _saveRecords() {
343 13
        $do_dbase = ShapeFile::supports_dbase();
344
345 13
        if ($do_dbase) {
346
            $dbf_name = $this->_getFilename('.dbf');
347
            if (file_exists($dbf_name)) {
348
                unlink($dbf_name);
349
            }
350
            $this->DBFFile = $this->_createDBFFile();
351
            if ($this->DBFFile === false) {
352
                return false;
353
            }
354
        }
355
356 13
        $offset = 50;
357 13
        if (is_array($this->records) && (count($this->records) > 0)) {
358 12
            foreach ($this->records as $index => $record) {
359
                //Save the record to the .shp file
360 12
                $record->saveToFile($this->SHPFile, $this->DBFFile, $index + 1);
361
362
                //Save the record to the .shx file
363 12
                fwrite($this->SHXFile, pack('N', $offset));
364 12
                fwrite($this->SHXFile, pack('N', $record->getContentLength()));
365 12
                $offset += (4 + $record->getContentLength());
366 12
            }
367 12
        }
368 13
        if ($do_dbase && !is_null($this->DBFFile)) {
369
            dbase_pack($this->DBFFile);
370
        }
371 13
    }
372
373 23
    private function _openFile($toWrite, $extension, $name) {
374 23
        $shp_name = $this->_getFilename($extension);
375 23
        $result = @fopen($shp_name, ($toWrite ? 'wb+' : 'rb'));
376 23
        if (!$result) {
377 2
            $this->setError(sprintf('It wasn\'t possible to open the %s file "%s"', $name, $shp_name));
378 2
            return false;
379
        }
380
381 21
        return $result;
382
    }
383
384 23
    private function _openSHPFile($toWrite = false) {
385 23
        $this->SHPFile = $this->_openFile($toWrite, '.shp', 'Shape');
386 23
        if (!$this->SHPFile) {
387 2
            return false;
388
        }
389 21
        return true;
390
    }
391
392 21
    private function _closeSHPFile() {
393 21
        if ($this->SHPFile) {
394 21
            fclose($this->SHPFile);
395 21
            $this->SHPFile = null;
396 21
        }
397 21
    }
398
399 13
    private function _openSHXFile($toWrite = false) {
400 13
        $this->SHXFile = $this->_openFile($toWrite, '.shx', 'Index');
401 13
        if (!$this->SHXFile) {
402
            return false;
403
        }
404 13
        return true;
405
    }
406
407 13
    private function _closeSHXFile() {
408 13
        if ($this->SHXFile) {
409 13
            fclose($this->SHXFile);
410 13
            $this->SHXFile = null;
411 13
        }
412 13
    }
413
414
    /**
415
     * Creates DBF file
416
     *
417
     * @return int|false
418
     */
419
    private function _createDBFFile()
420
    {
421
        if (count($this->DBFHeader) == 0) {
422
            return null;
423
        }
424
        $dbf_name = $this->_getFilename('.dbf');
425
        $result = @dbase_create($dbf_name, $this->DBFHeader);
426
        if ($result === false) {
427
            $this->setError(sprintf('It wasn\'t possible to create the DBase file "%s"', $dbf_name));
428
            return false;
429
        }
430
        return $result;
431
432
    }
433
434
    /**
435
     * Loads DBF file if supported
436
     *
437
     * @return bool
438
     */
439 21
    private function _openDBFFile($toWrite = false) {
440 21
        if (!ShapeFile::supports_dbase()) {
441 21
            return true;
442
        }
443
        $dbf_name = $this->_getFilename('.dbf');
444
        $checkFunction = $toWrite ? 'is_writable' : 'is_readable';
445
        if (($toWrite) && (!file_exists($dbf_name))) {
446
            if ($this->_createDBFFile() === false) {
447
                return false;
448
            }
449
        }
450
        if ($checkFunction($dbf_name)) {
451
            $this->DBFFile = @dbase_open($dbf_name, ($toWrite ? 2 : 0));
452
            if (!$this->DBFFile) {
453
                $this->setError(sprintf('It wasn\'t possible to open the DBase file "%s"', $dbf_name));
454
                return false;
455
            }
456
        } else {
457
            $this->setError(sprintf('It wasn\'t possible to find the DBase file "%s"', $dbf_name));
458
            return false;
459
        }
460
        return true;
461
    }
462
463 21
    private function _closeDBFFile() {
464 21
        if ($this->DBFFile) {
465
            dbase_close($this->DBFFile);
466
            $this->DBFFile = null;
467
        }
468 21
    }
469
470
    /**
471
     * Sets error message
472
     *
473
     * @param string $error
474
     *
475
     * @return void
476
     */
477 3
    public function setError($error) {
478 3
        $this->lastError = $error;
479 3
    }
480
481
    /**
482
     * Reads given number of bytes from SHP file
483
     *
484
     * @param integer $bytes
485
     *
486
     * @return string
487
     */
488 20
    public function readSHP($bytes)
489
    {
490 20
        return fread($this->SHPFile, $bytes);
491
    }
492
493
    /**
494
     * Checks whether file is at EOF
495
     *
496
     * @return bool
497
     */
498 19
    public function eofSHP()
499
    {
500 19
        return feof($this->SHPFile);
501
    }
502
503
    /**
504
     * Returns shape name
505
     *
506
     * @return string
507
     */
508 1
    public function getShapeName()
509
    {
510 1
        return Util::nameShape($this->shapeType);
511
    }
512
513
    /**
514
     * Check whether file contains measure data.
515
     *
516
     * For some reason this is distinguished by zero bouding box in the
517
     * specification.
518
     *
519
     * @return bool
520
     */
521 8
    public function hasMeasure()
522
    {
523 8
            return $this->boundingBox['mmin'] != 0 || $this->boundingBox['mmax'] != 0;
524
    }
525
}
526
527