Completed
Push — master ( 3425cb...75cab8 )
by Michal
03:10
created

ShapeFile::_openDBFFile()   C

Complexity

Conditions 9
Paths 15

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 55.8693

Importance

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