Completed
Push — master ( 0a8135...de4747 )
by Michal
02:57
created

ShapeFile::_openDBFFile()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 9.8312

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 4
eloc 14
c 4
b 0
f 0
nc 4
nop 0
dl 0
loc 18
ccs 4
cts 14
cp 0.2857
crap 9.8312
rs 9.2
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
    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;
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 integer $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 24
        $this->shapeType = $shapeType;
63 24
        $this->boundingBox = $boundingBox;
64 24
        $this->FileName = $FileName;
65 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).
66 24
    }
67
68
    /**
69
     * @param string $FileName
70
     */
71 22
    public function loadFromFile($FileName) {
72 22
        if (!empty($FileName)) {
73 22
            $this->FileName = $FileName;
74 22
            $result = $this->_openSHPFile();
75 22
        } else {
76
            /* We operate on buffer emulated by readSHP / eofSHP */
77
            $result = true;
78
        }
79
80 22
        if ($result && ($this->_openDBFFile())) {
81 20
            if (!$this->_loadHeaders()) {
82 1
                $this->_closeSHPFile();
83 1
                $this->_closeDBFFile();
84 1
                return false;
85
            }
86 19
            if (!$this->_loadRecords()) {
87
                $this->_closeSHPFile();
88
                $this->_closeDBFFile();
89
                return false;
90
            }
91 19
            $this->_closeSHPFile();
92 19
            $this->_closeDBFFile();
93 19
            return true;
94
        } else {
95 2
            return false;
96
        }
97
    }
98
99
    /**
100
     * @param string|null $FileName Name of file to open
101
     */
102 13
    public function saveToFile($FileName = null) {
103 13
        if (!is_null($FileName)) {
104 13
            $this->FileName = $FileName;
105 13
        }
106
107 13
        if (($this->_openSHPFile(true)) && ($this->_openSHXFile(true)) && ($this->_createDBFFile())) {
108 13
            $this->_saveHeaders();
109 13
            $this->_saveRecords();
110 13
            $this->_closeSHPFile();
111 13
            $this->_closeSHXFile();
112 13
            $this->_closeDBFFile();
113 13
        } else {
114
            return false;
115
        }
116 13
    }
117
118
    /**
119
     * Generates filename with given extension
120
     *
121
     * @param string $extension Extension to use (including dot)
122
     *
123
     * @return string
124
     */
125 23
    private function _getFilename($extension)
126
    {
127 23
        return str_replace('.*', $extension, $this->FileName);
128
    }
129
130
    /**
131
     * Updates bounding box based on SHPData
132
     *
133
     * @param string $type Type of box
134
     * @param array  $data ShapeRecord SHPData
135
     *
136
     * @return void
137
     */
138 12
    private function updateBBox($type, $data)
139
    {
140 12
        $min = $type.'min';
141 12
        $max = $type.'max';
142
143 12
        if (!isset($this->boundingBox[$min]) || $this->boundingBox[$min] == 0.0 || ($this->boundingBox[$min] > $data[$min])) {
144 12
            $this->boundingBox[$min] = $data[$min];
145 12
        }
146 12
        if (!isset($this->boundingBox[$max]) || $this->boundingBox[$max] == 0.0 || ($this->boundingBox[$max] < $data[$max])) {
147 12
            $this->boundingBox[$max] = $data[$max];
148 12
        }
149 12
    }
150
151
    /**
152
     * @param ShapeRecord $record
153
     */
154 12
    public function addRecord($record) {
155 12
        if ((isset($this->DBFHeader)) && (is_array($this->DBFHeader))) {
156 12
            $record->updateDBFInfo($this->DBFHeader);
157 12
        }
158
159 12
        $this->fileLength += ($record->getContentLength() + 4);
160 12
        $this->records[] = $record;
161 12
        $this->records[count($this->records) - 1]->recordNumber = count($this->records);
162
163 12
        $this->updateBBox('x', $record->SHPData);
164 12
        $this->updateBBox('y', $record->SHPData);
165
166 12
        if (in_array($this->shapeType, array(11, 13, 15, 18, 21, 23, 25, 28))) {
167 8
            $this->updateBBox('m', $record->SHPData);
168 8
        }
169
170 12
        if (in_array($this->shapeType, array(11, 13, 15, 18))) {
171 4
            $this->updateBBox('z', $record->SHPData);
172 4
        }
173
174 12
        return (count($this->records) - 1);
175
    }
176
177
    /**
178
     * @param integer $index
179
     */
180
    public function deleteRecord($index) {
181
        if (isset($this->records[$index])) {
182
            $this->fileLength -= ($this->records[$index]->getContentLength() + 4);
183
            $count = count($this->records) - 1;
184
            for ($i = $index; $i < $count; $i++) {
185
                $this->records[$i] = $this->records[$i + 1];
186
            }
187
            unset($this->records[count($this->records) - 1]);
188
            $this->_deleteRecordFromDBF($index);
189
        }
190
    }
191
192
    /**
193
     * Returns array defining fields in DBF file
194
     *
195
     * @return array See setDBFHeader for more information.
196
     */
197
    public function getDBFHeader() {
198
        return $this->DBFHeader;
199
    }
200
201
    /**
202
     * Changes array defining fields in DBF file, used in dbase_create call
203
     *
204
     * @param array $header An array of arrays, each array describing the
205
     *                      format of one field of the database. Each
206
     *                      field consists of a name, a character indicating
207
     *                      the field type, and optionally, a length,
208
     *                      a precision and a nullable flag.
209
     */
210 12
    public function setDBFHeader($header) {
211 12
        $this->DBFHeader = $header;
212
213 12
        $count = count($this->records);
214 12
        for ($i = 0; $i < $count; $i++) {
215
            $this->records[$i]->updateDBFInfo($header);
216
        }
217 12
    }
218
219
    /**
220
     * Lookups value in the DBF file and returs index
221
     *
222
     * @param string $field Field to match
223
     * @param mixed  $value Value to match
224
     *
225
     * @return integer
226
     */
227 1
    public function getIndexFromDBFData($field, $value) {
228 1
        foreach ($this->records as $index => $record) {
229 1
            if (isset($record->DBFData[$field]) &&
230
                (trim(strtoupper($record->DBFData[$field])) == strtoupper($value))
231 1
            ) {
232
                return $index;
233
            }
234 1
        }
235
236 1
        return -1;
237
    }
238
239
    private function _loadDBFHeader() {
240
        $DBFFile = fopen($this->_getFilename('.dbf'), 'r');
241
242
        $result = array();
243
        $i = 1;
244
        $inHeader = true;
245
246
        while ($inHeader) {
247
            if (!feof($DBFFile)) {
248
                $buff32 = fread($DBFFile, 32);
249
                if ($i > 1) {
250
                    if (substr($buff32, 0, 1) == chr(13)) {
251
                        $inHeader = false;
252
                    } else {
253
                        $pos = strpos(substr($buff32, 0, 10), chr(0));
254
                        $pos = ($pos == 0 ? 10 : $pos);
255
256
                        $fieldName = substr($buff32, 0, $pos);
257
                        $fieldType = substr($buff32, 11, 1);
258
                        $fieldLen = ord(substr($buff32, 16, 1));
259
                        $fieldDec = ord(substr($buff32, 17, 1));
260
261
                        array_push($result, array($fieldName, $fieldType, $fieldLen, $fieldDec));
262
                    }
263
                }
264
                $i++;
265
            } else {
266
                $inHeader = false;
267
            }
268
        }
269
270
        fclose($DBFFile);
271
        return($result);
272
    }
273
274
    private function _deleteRecordFromDBF($index) {
275
        if (@dbase_delete_record($this->DBFFile, $index)) {
276
            dbase_pack($this->DBFFile);
277
        }
278
    }
279
280 20
    private function _loadHeaders() {
281 20
        if (Util::loadData('N', $this->readSHP(4)) != ShapeFile::MAGIC) {
282 1
            $this->setError('Not a SHP file (file code mismatch)');
283 1
            return false;
284
        }
285
286
        /* Skip 20 unused bytes */
287 19
        $this->readSHP(20);
288
289 19
        $this->fileLength = Util::loadData('N', $this->readSHP(4));
290
291
        /* We currently ignore version */
292 19
        $this->readSHP(4);
293
294 19
        $this->shapeType = Util::loadData('V', $this->readSHP(4));
295
296 19
        $this->boundingBox = array();
297 19
        $this->boundingBox['xmin'] = Util::loadData('d', $this->readSHP(8));
298 19
        $this->boundingBox['ymin'] = Util::loadData('d', $this->readSHP(8));
299 19
        $this->boundingBox['xmax'] = Util::loadData('d', $this->readSHP(8));
300 19
        $this->boundingBox['ymax'] = Util::loadData('d', $this->readSHP(8));
301 19
        $this->boundingBox['zmin'] = Util::loadData('d', $this->readSHP(8));
302 19
        $this->boundingBox['zmax'] = Util::loadData('d', $this->readSHP(8));
303 19
        $this->boundingBox['mmin'] = Util::loadData('d', $this->readSHP(8));
304 19
        $this->boundingBox['mmax'] = Util::loadData('d', $this->readSHP(8));
305
306 19
        if (ShapeFile::supports_dbase()) {
307
            $this->DBFHeader = $this->_loadDBFHeader();
308
        }
309 19
        return true;
310
    }
311
312 13
    private function _saveBBoxRecord($file, $type) {
313 13
        fwrite($file, Util::packDouble(
314 13
            isset($this->boundingBox[$type]) ? $this->boundingBox[$type] : 0)
315 13
        );
316 13
    }
317
318 13
    private function _saveBBox($file) {
319 13
        $this->_saveBBoxRecord($file, 'xmin');
320 13
        $this->_saveBBoxRecord($file, 'ymin');
321 13
        $this->_saveBBoxRecord($file, 'xmax');
322 13
        $this->_saveBBoxRecord($file, 'ymax');
323 13
        $this->_saveBBoxRecord($file, 'zmin');
324 13
        $this->_saveBBoxRecord($file, 'zmax');
325 13
        $this->_saveBBoxRecord($file, 'mmin');
326 13
        $this->_saveBBoxRecord($file, 'mmax');
327 13
    }
328
329 13
    private function _saveHeaders() {
330 13
        fwrite($this->SHPFile, pack('NNNNNN', ShapeFile::MAGIC, 0, 0, 0, 0, 0));
331 13
        fwrite($this->SHPFile, pack('N', $this->fileLength));
332 13
        fwrite($this->SHPFile, pack('V', 1000));
333 13
        fwrite($this->SHPFile, pack('V', $this->shapeType));
334 13
        $this->_saveBBox($this->SHPFile);
335
336 13
        fwrite($this->SHXFile, pack('NNNNNN', ShapeFile::MAGIC, 0, 0, 0, 0, 0));
337 13
        fwrite($this->SHXFile, pack('N', 50 + 4 * count($this->records)));
338 13
        fwrite($this->SHXFile, pack('V', 1000));
339 13
        fwrite($this->SHXFile, pack('V', $this->shapeType));
340 13
        $this->_saveBBox($this->SHXFile);
341 13
    }
342
343 19
    private function _loadRecords() {
344
        /* Need to start at offset 100 */
345 19
        while (!$this->eofSHP()) {
346 19
            $record = new ShapeRecord(-1);
347 19
            $record->loadFromFile($this, $this->SHPFile, $this->DBFFile);
348 19
            if ($record->lastError != '') {
349
                $this->setError($record->lastError);
350
                return false;
351
            }
352 19
            if (($record->shapeType === false || $record->shapeType === '') && $this->eofSHP()) {
353 19
                break;
354
            }
355
356 19
            $this->records[] = $record;
357 19
        }
358 19
        return true;
359
    }
360
361 13
    private function _saveRecords() {
362 13
        $offset = 50;
363 13
        if (is_array($this->records) && (count($this->records) > 0)) {
364 12
            foreach ($this->records as $index => $record) {
365
                //Save the record to the .shp file
366 12
                $record->saveToFile($this->SHPFile, $this->DBFFile, $index + 1);
367
368
                //Save the record to the .shx file
369 12
                fwrite($this->SHXFile, pack('N', $offset));
370 12
                fwrite($this->SHXFile, pack('N', $record->getContentLength()));
371 12
                $offset += (4 + $record->getContentLength());
372 12
            }
373 12
        }
374 13
    }
375
376 23
    private function _openFile($toWrite, $extension, $name) {
377 23
        $shp_name = $this->_getFilename($extension);
378 23
        $result = @fopen($shp_name, ($toWrite ? 'wb+' : 'rb'));
379 23
        if (!$result) {
380 2
            $this->setError(sprintf('It wasn\'t possible to open the %s file "%s"', $name, $shp_name));
381 2
            return false;
382
        }
383
384 21
        return $result;
385
    }
386
387 23
    private function _openSHPFile($toWrite = false) {
388 23
        $this->SHPFile = $this->_openFile($toWrite, '.shp', 'Shape');
389 23
        if (!$this->SHPFile) {
390 2
            return false;
391
        }
392 21
        return true;
393
    }
394
395 21
    private function _closeSHPFile() {
396 21
        if ($this->SHPFile) {
397 21
            fclose($this->SHPFile);
398 21
            $this->SHPFile = null;
399 21
        }
400 21
    }
401
402 13
    private function _openSHXFile($toWrite = false) {
403 13
        $this->SHXFile = $this->_openFile($toWrite, '.shx', 'Index');
404 13
        if (!$this->SHXFile) {
405
            return false;
406
        }
407 13
        return true;
408
    }
409
410 13
    private function _closeSHXFile() {
411 13
        if ($this->SHXFile) {
412 13
            fclose($this->SHXFile);
413 13
            $this->SHXFile = null;
414 13
        }
415 13
    }
416
417
    /**
418
     * Creates DBF file
419
     *
420
     * @return bool
421
     */
422 13
    private function _createDBFFile()
423
    {
424 13
        if (!ShapeFile::supports_dbase() || count($this->DBFHeader) == 0) {
425 13
            $this->DBFFile = null;
426 13
            return true;
427
        }
428
        $dbf_name = $this->_getFilename('.dbf');
429
430
        /* Unlink existing file */
431
        if (file_exists($dbf_name)) {
432
            unlink($dbf_name);
433
        }
434
435
        /* Create new file */
436
        $this->DBFFile = @dbase_create($dbf_name, $this->DBFHeader);
437
        if ($this->DBFFile === false) {
438
            $this->setError(sprintf('It wasn\'t possible to create the DBase file "%s"', $dbf_name));
439
            return false;
440
        }
441
        return true;
442
443
    }
444
445
    /**
446
     * Loads DBF file if supported
447
     *
448
     * @return bool
449
     */
450 20
    private function _openDBFFile() {
451 20
        if (!ShapeFile::supports_dbase()) {
452 20
            $this->DBFFile = null;
453 20
            return true;
454
        }
455
        $dbf_name = $this->_getFilename('.dbf');
456
        if (is_readable($dbf_name)) {
457
            $this->DBFFile = @dbase_open($dbf_name, 0);
458
            if (!$this->DBFFile) {
459
                $this->setError(sprintf('It wasn\'t possible to open the DBase file "%s"', $dbf_name));
460
                return false;
461
            }
462
        } else {
463
            $this->setError(sprintf('It wasn\'t possible to find the DBase file "%s"', $dbf_name));
464
            return false;
465
        }
466
        return true;
467
    }
468
469 21
    private function _closeDBFFile() {
470 21
        if ($this->DBFFile) {
471
            dbase_close($this->DBFFile);
472
            $this->DBFFile = null;
473
        }
474 21
    }
475
476
    /**
477
     * Sets error message
478
     *
479
     * @param string $error
480
     *
481
     * @return void
482
     */
483 3
    public function setError($error) {
484 3
        $this->lastError = $error;
485 3
    }
486
487
    /**
488
     * Reads given number of bytes from SHP file
489
     *
490
     * @param integer $bytes
491
     *
492
     * @return string
493
     */
494 20
    public function readSHP($bytes)
495
    {
496 20
        return fread($this->SHPFile, $bytes);
497
    }
498
499
    /**
500
     * Checks whether file is at EOF
501
     *
502
     * @return bool
503
     */
504 19
    public function eofSHP()
505
    {
506 19
        return feof($this->SHPFile);
507
    }
508
509
    /**
510
     * Returns shape name
511
     *
512
     * @return string
513
     */
514 1
    public function getShapeName()
515
    {
516 1
        return Util::nameShape($this->shapeType);
517
    }
518
519
    /**
520
     * Check whether file contains measure data.
521
     *
522
     * For some reason this is distinguished by zero bouding box in the
523
     * specification.
524
     *
525
     * @return bool
526
     */
527 8
    public function hasMeasure()
528
    {
529 8
            return $this->boundingBox['mmin'] != 0 || $this->boundingBox['mmax'] != 0;
530
    }
531
}
532
533