Completed
Push — master ( 1a29e8...2fb798 )
by Michal
03:08
created

ShapeFile::_getFilename()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
c 1
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
    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->_openDBFFile(true))) {
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
        $do_dbase = ShapeFile::supports_dbase();
363
364 13
        if ($do_dbase) {
365
            $dbf_name = $this->_getFilename('.dbf');
366
            if (file_exists($dbf_name)) {
367
                unlink($dbf_name);
368
            }
369
            $this->DBFFile = $this->_createDBFFile();
370
            if ($this->DBFFile === false) {
371
                return false;
372
            }
373
            /* No dbase data to save */
374
            if (is_null($this->DBFFile)) {
375
                $do_dbase = false;
376
            }
377
        }
378
379 13
        $offset = 50;
380 13
        if (is_array($this->records) && (count($this->records) > 0)) {
381 12
            foreach ($this->records as $index => $record) {
382
                //Save the record to the .shp file
383 12
                $record->saveToFile($this->SHPFile, $this->DBFFile, $index + 1);
384
385
                //Save the record to the .shx file
386 12
                fwrite($this->SHXFile, pack('N', $offset));
387 12
                fwrite($this->SHXFile, pack('N', $record->getContentLength()));
388 12
                $offset += (4 + $record->getContentLength());
389 12
            }
390 12
        }
391 13
        if ($do_dbase) {
392
            dbase_pack($this->DBFFile);
393
        }
394 13
    }
395
396 23
    private function _openFile($toWrite, $extension, $name) {
397 23
        $shp_name = $this->_getFilename($extension);
398 23
        $result = @fopen($shp_name, ($toWrite ? 'wb+' : 'rb'));
399 23
        if (!$result) {
400 2
            $this->setError(sprintf('It wasn\'t possible to open the %s file "%s"', $name, $shp_name));
401 2
            return false;
402
        }
403
404 21
        return $result;
405
    }
406
407 23
    private function _openSHPFile($toWrite = false) {
408 23
        $this->SHPFile = $this->_openFile($toWrite, '.shp', 'Shape');
409 23
        if (!$this->SHPFile) {
410 2
            return false;
411
        }
412 21
        return true;
413
    }
414
415 21
    private function _closeSHPFile() {
416 21
        if ($this->SHPFile) {
417 21
            fclose($this->SHPFile);
418 21
            $this->SHPFile = null;
419 21
        }
420 21
    }
421
422 13
    private function _openSHXFile($toWrite = false) {
423 13
        $this->SHXFile = $this->_openFile($toWrite, '.shx', 'Index');
424 13
        if (!$this->SHXFile) {
425
            return false;
426
        }
427 13
        return true;
428
    }
429
430 13
    private function _closeSHXFile() {
431 13
        if ($this->SHXFile) {
432 13
            fclose($this->SHXFile);
433 13
            $this->SHXFile = null;
434 13
        }
435 13
    }
436
437
    /**
438
     * Creates DBF file
439
     *
440
     * @return int|false
441
     */
442
    private function _createDBFFile()
443
    {
444
        if (count($this->DBFHeader) == 0) {
445
            return null;
446
        }
447
        $dbf_name = $this->_getFilename('.dbf');
448
        $result = @dbase_create($dbf_name, $this->DBFHeader);
449
        if ($result === false) {
450
            $this->setError(sprintf('It wasn\'t possible to create the DBase file "%s"', $dbf_name));
451
            return false;
452
        }
453
        return $result;
454
455
    }
456
457
    /**
458
     * Loads DBF file if supported
459
     *
460
     * @return bool
461
     */
462 21
    private function _openDBFFile($toWrite = false) {
463 21
        if (!ShapeFile::supports_dbase()) {
464 21
            return true;
465
        }
466
        $dbf_name = $this->_getFilename('.dbf');
467
        $checkFunction = $toWrite ? 'is_writable' : 'is_readable';
468
        if (($toWrite) && (!file_exists($dbf_name))) {
469
            if ($this->_createDBFFile() === false) {
470
                return false;
471
            }
472
        }
473
        if ($checkFunction($dbf_name)) {
474
            $this->DBFFile = @dbase_open($dbf_name, ($toWrite ? 2 : 0));
475
            if (!$this->DBFFile) {
476
                $this->setError(sprintf('It wasn\'t possible to open the DBase file "%s"', $dbf_name));
477
                return false;
478
            }
479
        } else {
480
            $this->setError(sprintf('It wasn\'t possible to find the DBase file "%s"', $dbf_name));
481
            return false;
482
        }
483
        return true;
484
    }
485
486 21
    private function _closeDBFFile() {
487 21
        if ($this->DBFFile) {
488
            dbase_close($this->DBFFile);
489
            $this->DBFFile = null;
490
        }
491 21
    }
492
493
    /**
494
     * Sets error message
495
     *
496
     * @param string $error
497
     *
498
     * @return void
499
     */
500 3
    public function setError($error) {
501 3
        $this->lastError = $error;
502 3
    }
503
504
    /**
505
     * Reads given number of bytes from SHP file
506
     *
507
     * @param integer $bytes
508
     *
509
     * @return string
510
     */
511 20
    public function readSHP($bytes)
512
    {
513 20
        return fread($this->SHPFile, $bytes);
514
    }
515
516
    /**
517
     * Checks whether file is at EOF
518
     *
519
     * @return bool
520
     */
521 19
    public function eofSHP()
522
    {
523 19
        return feof($this->SHPFile);
524
    }
525
526
    /**
527
     * Returns shape name
528
     *
529
     * @return string
530
     */
531 1
    public function getShapeName()
532
    {
533 1
        return Util::nameShape($this->shapeType);
534
    }
535
536
    /**
537
     * Check whether file contains measure data.
538
     *
539
     * For some reason this is distinguished by zero bouding box in the
540
     * specification.
541
     *
542
     * @return bool
543
     */
544 8
    public function hasMeasure()
545
    {
546 8
            return $this->boundingBox['mmin'] != 0 || $this->boundingBox['mmax'] != 0;
547
    }
548
}
549
550