Completed
Push — master ( 67a9e5...5289c4 )
by Michal
03:56
created

ShapeFile   D

Complexity

Total Complexity 90

Size/Duplication

Total Lines 618
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 70.48%

Importance

Changes 0
Metric Value
dl 0
loc 618
ccs 191
cts 271
cp 0.7048
rs 4.7996
c 0
b 0
f 0
wmc 90
lcom 1
cbo 2

32 Methods

Rating   Name   Duplication   Size   Complexity  
A supports_dbase() 0 4 1
A __construct() 0 7 1
B loadFromFile() 0 31 6
B saveToFile() 0 16 5
A _getFilename() 0 4 1
B updateBBox() 0 12 7
B addRecord() 0 23 5
A deleteRecord() 0 12 3
A getDBFHeader() 0 4 1
A setDBFHeader() 0 9 2
A getIndexFromDBFData() 0 12 4
B _loadDBFHeader() 0 36 6
A _deleteRecordFromDBF() 0 6 2
B _loadHeaders() 0 34 3
A _saveBBoxRecord() 0 6 2
A _saveBBox() 0 11 1
A _saveHeaders() 0 14 1
B _loadRecords() 0 20 6
A _saveRecords() 0 15 4
A _openFile() 0 12 3
A _openSHPFile() 0 9 2
A _closeSHPFile() 0 7 2
A _openSHXFile() 0 9 2
A _closeSHXFile() 0 7 2
B _createDBFFile() 0 24 6
B _openDBFFile() 0 23 4
A _closeDBFFile() 0 7 2
A setError() 0 4 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
23
namespace PhpMyAdmin\ShapeFile;
24
25
/**
26
 * ShapeFile class.
27
 */
28
class ShapeFile
29
{
30
    const MAGIC = 0x270a;
31
32
    public $FileName;
33
34
    private $SHPFile = null;
35
    private $SHXFile = null;
36
    private $DBFFile = null;
37
38
    private $DBFHeader;
39
40
    public $lastError = '';
41
42
    public $boundingBox = array('xmin' => 0.0, 'ymin' => 0.0, 'xmax' => 0.0, 'ymax' => 0.0);
43
    private $fileLength = 0;
44
    public $shapeType = 0;
45
46
    public $records = array();
47
48
    /**
49
     * Checks whether dbase manipuations are supported.
50
     *
51
     * @return bool
52
     */
53 21
    public static function supports_dbase()
54
    {
55 21
        return extension_loaded('dbase');
56
    }
57
58
    /**
59
     * @param int $shapeType
60
     */
61 24
    public function __construct($shapeType, $boundingBox = array('xmin' => 0.0, 'ymin' => 0.0, 'xmax' => 0.0, 'ymax' => 0.0), $FileName = null)
62
    {
63 24
        $this->shapeType = $shapeType;
64 24
        $this->boundingBox = $boundingBox;
65 24
        $this->FileName = $FileName;
66 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).
67 24
    }
68
69
    /**
70
     * Loads shapefile and dbase (if supported).
71
     *
72
     * @param string $FileName File mask to load (eg. example.*)
73
     */
74 22
    public function loadFromFile($FileName)
75
    {
76 22
        if (!empty($FileName)) {
77 22
            $this->FileName = $FileName;
78 22
            $result = $this->_openSHPFile();
79 22
        } else {
80
            /* We operate on buffer emulated by readSHP / eofSHP */
81
            $result = true;
82
        }
83
84 22
        if ($result && ($this->_openDBFFile())) {
85 20
            if (!$this->_loadHeaders()) {
86 1
                $this->_closeSHPFile();
87 1
                $this->_closeDBFFile();
88
89 1
                return false;
90
            }
91 19
            if (!$this->_loadRecords()) {
92
                $this->_closeSHPFile();
93
                $this->_closeDBFFile();
94
95
                return false;
96
            }
97 19
            $this->_closeSHPFile();
98 19
            $this->_closeDBFFile();
99
100 19
            return true;
101
        } else {
102 2
            return false;
103
        }
104
    }
105
106
    /**
107
     * Saves shapefile.
108
     *
109
     * @param string|null $FileName Name of file, otherwise existing is used
110
     */
111 13
    public function saveToFile($FileName = null)
112
    {
113 13
        if (!is_null($FileName)) {
114 13
            $this->FileName = $FileName;
115 13
        }
116
117 13
        if (($this->_openSHPFile(true)) && ($this->_openSHXFile(true)) && ($this->_createDBFFile())) {
118 13
            $this->_saveHeaders();
119 13
            $this->_saveRecords();
120 13
            $this->_closeSHPFile();
121 13
            $this->_closeSHXFile();
122 13
            $this->_closeDBFFile();
123 13
        } else {
124
            return false;
125
        }
126 13
    }
127
128
    /**
129
     * Generates filename with given extension.
130
     *
131
     * @param string $extension Extension to use (including dot)
132
     *
133
     * @return string
134
     */
135 23
    private function _getFilename($extension)
136
    {
137 23
        return str_replace('.*', $extension, $this->FileName);
138
    }
139
140
    /**
141
     * Updates bounding box based on SHPData.
142
     *
143
     * @param string $type Type of box
144
     * @param array  $data ShapeRecord SHPData
145
     */
146 12
    private function updateBBox($type, $data)
147
    {
148 12
        $min = $type . 'min';
149 12
        $max = $type . 'max';
150
151 12
        if (!isset($this->boundingBox[$min]) || $this->boundingBox[$min] == 0.0 || ($this->boundingBox[$min] > $data[$min])) {
152 12
            $this->boundingBox[$min] = $data[$min];
153 12
        }
154 12
        if (!isset($this->boundingBox[$max]) || $this->boundingBox[$max] == 0.0 || ($this->boundingBox[$max] < $data[$max])) {
155 12
            $this->boundingBox[$max] = $data[$max];
156 12
        }
157 12
    }
158
159
    /**
160
     * Adds record to shape file.
161
     *
162
     * @param ShapeRecord $record
163
     *
164
     * @return int Number of added record
165
     */
166 12
    public function addRecord($record)
167
    {
168 12
        if ((isset($this->DBFHeader)) && (is_array($this->DBFHeader))) {
169 12
            $record->updateDBFInfo($this->DBFHeader);
170 12
        }
171
172 12
        $this->fileLength += ($record->getContentLength() + 4);
173 12
        $this->records[] = $record;
174 12
        $this->records[count($this->records) - 1]->recordNumber = count($this->records);
175
176 12
        $this->updateBBox('x', $record->SHPData);
177 12
        $this->updateBBox('y', $record->SHPData);
178
179 12
        if (in_array($this->shapeType, array(11, 13, 15, 18, 21, 23, 25, 28))) {
180 8
            $this->updateBBox('m', $record->SHPData);
181 8
        }
182
183 12
        if (in_array($this->shapeType, array(11, 13, 15, 18))) {
184 4
            $this->updateBBox('z', $record->SHPData);
185 4
        }
186
187 12
        return count($this->records) - 1;
188
    }
189
190
    /**
191
     * Deletes record from shapefile.
192
     *
193
     * @param int $index
194
     */
195
    public function deleteRecord($index)
196
    {
197
        if (isset($this->records[$index])) {
198
            $this->fileLength -= ($this->records[$index]->getContentLength() + 4);
199
            $count = count($this->records) - 1;
200
            for ($i = $index; $i < $count; ++$i) {
201
                $this->records[$i] = $this->records[$i + 1];
202
            }
203
            unset($this->records[count($this->records) - 1]);
204
            $this->_deleteRecordFromDBF($index);
205
        }
206
    }
207
208
    /**
209
     * Returns array defining fields in DBF file.
210
     *
211
     * @return array see setDBFHeader for more information
212
     */
213
    public function getDBFHeader()
214
    {
215
        return $this->DBFHeader;
216
    }
217
218
    /**
219
     * Changes array defining fields in DBF file, used in dbase_create call.
220
     *
221
     * @param array $header An array of arrays, each array describing the
222
     *                      format of one field of the database. Each
223
     *                      field consists of a name, a character indicating
224
     *                      the field type, and optionally, a length,
225
     *                      a precision and a nullable flag.
226
     */
227 12
    public function setDBFHeader($header)
228
    {
229 12
        $this->DBFHeader = $header;
230
231 12
        $count = count($this->records);
232 12
        for ($i = 0; $i < $count; ++$i) {
233
            $this->records[$i]->updateDBFInfo($header);
234
        }
235 12
    }
236
237
    /**
238
     * Lookups value in the DBF file and returs index.
239
     *
240
     * @param string $field Field to match
241
     * @param mixed  $value Value to match
242
     *
243
     * @return int
244
     */
245 1
    public function getIndexFromDBFData($field, $value)
246
    {
247 1
        foreach ($this->records as $index => $record) {
248 1
            if (isset($record->DBFData[$field]) &&
249
                (trim(strtoupper($record->DBFData[$field])) == strtoupper($value))
250 1
            ) {
251
                return $index;
252
            }
253 1
        }
254
255 1
        return -1;
256
    }
257
258
    /**
259
     * Loads DBF metadata.
260
     */
261
    private function _loadDBFHeader()
262
    {
263
        $DBFFile = fopen($this->_getFilename('.dbf'), 'r');
264
265
        $result = array();
266
        $i = 1;
267
        $inHeader = true;
268
269
        while ($inHeader) {
270
            if (!feof($DBFFile)) {
271
                $buff32 = fread($DBFFile, 32);
272
                if ($i > 1) {
273
                    if (substr($buff32, 0, 1) == chr(13)) {
274
                        $inHeader = false;
275
                    } else {
276
                        $pos = strpos(substr($buff32, 0, 10), chr(0));
277
                        $pos = ($pos == 0 ? 10 : $pos);
278
279
                        $fieldName = substr($buff32, 0, $pos);
280
                        $fieldType = substr($buff32, 11, 1);
281
                        $fieldLen = ord(substr($buff32, 16, 1));
282
                        $fieldDec = ord(substr($buff32, 17, 1));
283
284
                        array_push($result, array($fieldName, $fieldType, $fieldLen, $fieldDec));
285
                    }
286
                }
287
                ++$i;
288
            } else {
289
                $inHeader = false;
290
            }
291
        }
292
293
        fclose($DBFFile);
294
295
        return $result;
296
    }
297
298
    /**
299
     * Deletes record from the DBF file.
300
     *
301
     * @param int $index
302
     */
303
    private function _deleteRecordFromDBF($index)
304
    {
305
        if (@dbase_delete_record($this->DBFFile, $index)) {
306
            dbase_pack($this->DBFFile);
307
        }
308
    }
309
310
    /**
311
     * Loads SHP file metadata.
312
     *
313
     * @return bool
314
     */
315 20
    private function _loadHeaders()
316
    {
317 20
        if (Util::loadData('N', $this->readSHP(4)) != self::MAGIC) {
318 1
            $this->setError('Not a SHP file (file code mismatch)');
319
320 1
            return false;
321
        }
322
323
        /* Skip 20 unused bytes */
324 19
        $this->readSHP(20);
325
326 19
        $this->fileLength = Util::loadData('N', $this->readSHP(4));
327
328
        /* We currently ignore version */
329 19
        $this->readSHP(4);
330
331 19
        $this->shapeType = Util::loadData('V', $this->readSHP(4));
332
333 19
        $this->boundingBox = array();
334 19
        $this->boundingBox['xmin'] = Util::loadData('d', $this->readSHP(8));
335 19
        $this->boundingBox['ymin'] = Util::loadData('d', $this->readSHP(8));
336 19
        $this->boundingBox['xmax'] = Util::loadData('d', $this->readSHP(8));
337 19
        $this->boundingBox['ymax'] = Util::loadData('d', $this->readSHP(8));
338 19
        $this->boundingBox['zmin'] = Util::loadData('d', $this->readSHP(8));
339 19
        $this->boundingBox['zmax'] = Util::loadData('d', $this->readSHP(8));
340 19
        $this->boundingBox['mmin'] = Util::loadData('d', $this->readSHP(8));
341 19
        $this->boundingBox['mmax'] = Util::loadData('d', $this->readSHP(8));
342
343 19
        if (self::supports_dbase()) {
344
            $this->DBFHeader = $this->_loadDBFHeader();
345
        }
346
347 19
        return true;
348
    }
349
350
    /**
351
     * Saves bounding box record, possibly using 0 instead of not set values.
352
     *
353
     * @param file   $file File object
354
     * @param string $type Bounding box dimension (eg. xmax, mmin...)
355
     */
356 13
    private function _saveBBoxRecord($file, $type)
357
    {
358 13
        fwrite($file, Util::packDouble(
359 13
            isset($this->boundingBox[$type]) ? $this->boundingBox[$type] : 0)
360 13
        );
361 13
    }
362
363
    /**
364
     * Saves bounding box to a file.
365
     *
366
     * @param file $file File object
367
     */
368 13
    private function _saveBBox($file)
369
    {
370 13
        $this->_saveBBoxRecord($file, 'xmin');
371 13
        $this->_saveBBoxRecord($file, 'ymin');
372 13
        $this->_saveBBoxRecord($file, 'xmax');
373 13
        $this->_saveBBoxRecord($file, 'ymax');
374 13
        $this->_saveBBoxRecord($file, 'zmin');
375 13
        $this->_saveBBoxRecord($file, 'zmax');
376 13
        $this->_saveBBoxRecord($file, 'mmin');
377 13
        $this->_saveBBoxRecord($file, 'mmax');
378 13
    }
379
380
    /**
381
     * Saves SHP and SHX file metadata.
382
     */
383 13
    private function _saveHeaders()
384
    {
385 13
        fwrite($this->SHPFile, pack('NNNNNN', self::MAGIC, 0, 0, 0, 0, 0));
386 13
        fwrite($this->SHPFile, pack('N', $this->fileLength));
387 13
        fwrite($this->SHPFile, pack('V', 1000));
388 13
        fwrite($this->SHPFile, pack('V', $this->shapeType));
389 13
        $this->_saveBBox($this->SHPFile);
0 ignored issues
show
Bug introduced by
It seems like $this->SHPFile can also be of type false or null; however, PhpMyAdmin\ShapeFile\ShapeFile::_saveBBox() does only seem to accept object<PhpMyAdmin\ShapeFile\file>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
390
391 13
        fwrite($this->SHXFile, pack('NNNNNN', self::MAGIC, 0, 0, 0, 0, 0));
392 13
        fwrite($this->SHXFile, pack('N', 50 + 4 * count($this->records)));
393 13
        fwrite($this->SHXFile, pack('V', 1000));
394 13
        fwrite($this->SHXFile, pack('V', $this->shapeType));
395 13
        $this->_saveBBox($this->SHXFile);
0 ignored issues
show
Bug introduced by
It seems like $this->SHXFile can also be of type false or null; however, PhpMyAdmin\ShapeFile\ShapeFile::_saveBBox() does only seem to accept object<PhpMyAdmin\ShapeFile\file>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
396 13
    }
397
398
    /**
399
     * Loads records from SHP file (and DBF).
400
     *
401
     * @return bool
402
     */
403 19
    private function _loadRecords()
404
    {
405
        /* Need to start at offset 100 */
406 19
        while (!$this->eofSHP()) {
407 19
            $record = new ShapeRecord(-1);
408 19
            $record->loadFromFile($this, $this->SHPFile, $this->DBFFile);
409 19
            if ($record->lastError != '') {
410
                $this->setError($record->lastError);
411
412
                return false;
413
            }
414 19
            if (($record->shapeType === false || $record->shapeType === '') && $this->eofSHP()) {
415 19
                break;
416
            }
417
418 19
            $this->records[] = $record;
419 19
        }
420
421 19
        return true;
422
    }
423
424
    /**
425
     * Saves records to SHP and SHX files.
426
     */
427 13
    private function _saveRecords()
428
    {
429 13
        $offset = 50;
430 13
        if (is_array($this->records) && (count($this->records) > 0)) {
431 12
            foreach ($this->records as $index => $record) {
432
                //Save the record to the .shp file
433 12
                $record->saveToFile($this->SHPFile, $this->DBFFile, $index + 1);
434
435
                //Save the record to the .shx file
436 12
                fwrite($this->SHXFile, pack('N', $offset));
437 12
                fwrite($this->SHXFile, pack('N', $record->getContentLength()));
438 12
                $offset += (4 + $record->getContentLength());
439 12
            }
440 12
        }
441 13
    }
442
443
    /**
444
     * Generic interface to open files.
445
     *
446
     * @param bool   $toWrite   Whether file should be opened for writing
447
     * @param string $extension File extension
448
     * @param string $name      Verbose file name to report errors
449
     *
450
     * @return file|false File handle
451
     */
452 23
    private function _openFile($toWrite, $extension, $name)
453
    {
454 23
        $shp_name = $this->_getFilename($extension);
455 23
        $result = @fopen($shp_name, ($toWrite ? 'wb+' : 'rb'));
456 23
        if (!$result) {
457 2
            $this->setError(sprintf('It wasn\'t possible to open the %s file "%s"', $name, $shp_name));
458
459 2
            return false;
460
        }
461
462 21
        return $result;
463
    }
464
465
    /**
466
     * Opens SHP file.
467
     *
468
     * @param bool $toWrite Whether file should be opened for writing
469
     *
470
     * @return bool
471
     */
472 23
    private function _openSHPFile($toWrite = false)
473
    {
474 23
        $this->SHPFile = $this->_openFile($toWrite, '.shp', 'Shape');
475 23
        if (!$this->SHPFile) {
476 2
            return false;
477
        }
478
479 21
        return true;
480
    }
481
482
    /**
483
     * Closes SHP file.
484
     */
485 21
    private function _closeSHPFile()
486
    {
487 21
        if ($this->SHPFile) {
488 21
            fclose($this->SHPFile);
489 21
            $this->SHPFile = null;
490 21
        }
491 21
    }
492
493
    /**
494
     * Opens SHX file.
495
     *
496
     * @param bool $toWrite Whether file should be opened for writing
497
     *
498
     * @return bool
499
     */
500 13
    private function _openSHXFile($toWrite = false)
501
    {
502 13
        $this->SHXFile = $this->_openFile($toWrite, '.shx', 'Index');
503 13
        if (!$this->SHXFile) {
504
            return false;
505
        }
506
507 13
        return true;
508
    }
509
510
    /**
511
     * Closes SHX file.
512
     */
513 13
    private function _closeSHXFile()
514
    {
515 13
        if ($this->SHXFile) {
516 13
            fclose($this->SHXFile);
517 13
            $this->SHXFile = null;
518 13
        }
519 13
    }
520
521
    /**
522
     * Creates DBF file.
523
     *
524
     * @return bool
525
     */
526 13
    private function _createDBFFile()
527
    {
528 13
        if (!self::supports_dbase() || !is_array($this->DBFHeader) || count($this->DBFHeader) == 0) {
529 13
            $this->DBFFile = null;
530
531 13
            return true;
532
        }
533
        $dbf_name = $this->_getFilename('.dbf');
534
535
        /* Unlink existing file */
536
        if (file_exists($dbf_name)) {
537
            unlink($dbf_name);
538
        }
539
540
        /* Create new file */
541
        $this->DBFFile = @dbase_create($dbf_name, $this->DBFHeader);
542
        if ($this->DBFFile === false) {
543
            $this->setError(sprintf('It wasn\'t possible to create the DBase file "%s"', $dbf_name));
544
545
            return false;
546
        }
547
548
        return true;
549
    }
550
551
    /**
552
     * Loads DBF file if supported.
553
     *
554
     * @return bool
555
     */
556 20
    private function _openDBFFile()
557
    {
558 20
        if (!self::supports_dbase()) {
559 20
            $this->DBFFile = null;
560
561 20
            return true;
562
        }
563
        $dbf_name = $this->_getFilename('.dbf');
564
        if (is_readable($dbf_name)) {
565
            $this->DBFFile = @dbase_open($dbf_name, 0);
566
            if (!$this->DBFFile) {
567
                $this->setError(sprintf('It wasn\'t possible to open the DBase file "%s"', $dbf_name));
568
569
                return false;
570
            }
571
        } else {
572
            $this->setError(sprintf('It wasn\'t possible to find the DBase file "%s"', $dbf_name));
573
574
            return false;
575
        }
576
577
        return true;
578
    }
579
580
    /**
581
     * Closes DBF file.
582
     */
583 21
    private function _closeDBFFile()
584
    {
585 21
        if ($this->DBFFile) {
586
            dbase_close($this->DBFFile);
587
            $this->DBFFile = null;
588
        }
589 21
    }
590
591
    /**
592
     * Sets error message.
593
     *
594
     * @param string $error
595
     */
596 3
    public function setError($error)
597
    {
598 3
        $this->lastError = $error;
599 3
    }
600
601
    /**
602
     * Reads given number of bytes from SHP file.
603
     *
604
     * @param int $bytes
605
     *
606
     * @return string
607
     */
608 20
    public function readSHP($bytes)
609
    {
610 20
        return fread($this->SHPFile, $bytes);
611
    }
612
613
    /**
614
     * Checks whether file is at EOF.
615
     *
616
     * @return bool
617
     */
618 19
    public function eofSHP()
619
    {
620 19
        return feof($this->SHPFile);
621
    }
622
623
    /**
624
     * Returns shape name.
625
     *
626
     * @return string
627
     */
628 1
    public function getShapeName()
629
    {
630 1
        return Util::nameShape($this->shapeType);
631
    }
632
633
    /**
634
     * Check whether file contains measure data.
635
     *
636
     * For some reason this is distinguished by zero bouding box in the
637
     * specification.
638
     *
639
     * @return bool
640
     */
641 8
    public function hasMeasure()
642
    {
643 8
        return $this->boundingBox['mmin'] != 0 || $this->boundingBox['mmax'] != 0;
644
    }
645
}
646