1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* phpMyAdmin ShapeFile library |
7
|
|
|
* <https://github.com/phpmyadmin/shapefile/>. |
8
|
|
|
* |
9
|
|
|
* Copyright 2006-2007 Ovidio <ovidio AT users.sourceforge.net> |
10
|
|
|
* Copyright 2016 - 2017 Michal Čihař <[email protected]> |
11
|
|
|
* |
12
|
|
|
* This program is free software; you can redistribute it and/or |
13
|
|
|
* modify it under the terms of the GNU General Public License |
14
|
|
|
* as published by the Free Software Foundation. |
15
|
|
|
* |
16
|
|
|
* This program is distributed in the hope that it will be useful, |
17
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
18
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
19
|
|
|
* GNU General Public License for more details. |
20
|
|
|
* |
21
|
|
|
* You should have received a copy of the GNU General Public License |
22
|
|
|
* along with this program; if not, you can download one from |
23
|
|
|
* https://www.gnu.org/copyleft/gpl.html. |
24
|
|
|
*/ |
25
|
|
|
|
26
|
|
|
namespace PhpMyAdmin\ShapeFile; |
27
|
|
|
|
28
|
|
|
use function array_values; |
29
|
|
|
use function count; |
30
|
|
|
use function dbase_add_record; |
|
|
|
|
31
|
|
|
use function dbase_get_record_with_names; |
|
|
|
|
32
|
|
|
use function dbase_numrecords; |
|
|
|
|
33
|
|
|
use function dbase_replace_record; |
|
|
|
|
34
|
|
|
use function fwrite; |
35
|
|
|
use function in_array; |
36
|
|
|
use function is_array; |
37
|
|
|
use function pack; |
38
|
|
|
use function strlen; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* ShapeFile record class. |
42
|
|
|
*/ |
43
|
|
|
class ShapeRecord |
44
|
|
|
{ |
45
|
|
|
/** @var resource */ |
46
|
|
|
private $shpFile; |
47
|
|
|
|
48
|
|
|
private ShapeFile|null $shapeFile = null; |
49
|
|
|
|
50
|
|
|
private int $size = 0; |
51
|
|
|
|
52
|
|
|
private int $read = 0; |
53
|
|
|
|
54
|
|
|
public int $recordNumber = 0; |
55
|
|
|
|
56
|
|
|
public string $lastError = ''; |
57
|
|
|
|
58
|
|
|
/** @var mixed[] */ |
59
|
|
|
public array $shpData = []; |
60
|
|
|
|
61
|
|
|
/** @var mixed[] */ |
62
|
|
|
public array $dbfData = []; |
63
|
|
|
|
64
|
92 |
|
public function __construct(public ShapeType $shapeType) |
65
|
|
|
{ |
66
|
92 |
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Loads record from files. |
70
|
|
|
* |
71
|
|
|
* @param ShapeFile $shapeFile The ShapeFile object |
72
|
|
|
* @param resource|false $dbfFile Opened DBF file |
73
|
|
|
*/ |
74
|
88 |
|
public function loadFromFile(ShapeFile $shapeFile, $dbfFile): void |
75
|
|
|
{ |
76
|
88 |
|
$this->shapeFile = $shapeFile; |
77
|
88 |
|
$this->loadHeaders(); |
78
|
|
|
|
79
|
|
|
/* No header read */ |
80
|
88 |
|
if ($this->read === 0) { |
81
|
88 |
|
return; |
82
|
|
|
} |
83
|
|
|
|
84
|
88 |
|
match ($this->shapeType) { |
85
|
88 |
|
ShapeType::Null => $this->loadNullRecord(), |
|
|
|
|
86
|
88 |
|
ShapeType::Point => $this->loadPointRecord(), |
|
|
|
|
87
|
72 |
|
ShapeType::PointM => $this->loadPointMRecord(), |
|
|
|
|
88
|
68 |
|
ShapeType::PointZ => $this->loadPointZRecord(), |
|
|
|
|
89
|
64 |
|
ShapeType::PolyLine => $this->loadPolyLineRecord(), |
|
|
|
|
90
|
51 |
|
ShapeType::PolyLineM => $this->loadPolyLineMRecord(), |
|
|
|
|
91
|
47 |
|
ShapeType::PolyLineZ => $this->loadPolyLineZRecord(), |
|
|
|
|
92
|
43 |
|
ShapeType::Polygon => $this->loadPolygonRecord(), |
|
|
|
|
93
|
28 |
|
ShapeType::PolygonM => $this->loadPolygonMRecord(), |
|
|
|
|
94
|
24 |
|
ShapeType::PolygonZ => $this->loadPolygonZRecord(), |
|
|
|
|
95
|
16 |
|
ShapeType::MultiPoint => $this->loadMultiPointRecord(), |
|
|
|
|
96
|
12 |
|
ShapeType::MultiPointM => $this->loadMultiPointMRecord(), |
|
|
|
|
97
|
8 |
|
ShapeType::MultiPointZ => $this->loadMultiPointZRecord(), |
|
|
|
|
98
|
|
|
default => $this->reportInvalidShapeTypeError(), |
|
|
|
|
99
|
88 |
|
}; |
100
|
|
|
|
101
|
|
|
/* We need to skip rest of the record */ |
102
|
88 |
|
while ($this->read < $this->size) { |
103
|
24 |
|
$this->loadData('V', 4); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/* Check if we didn't read too much */ |
107
|
88 |
|
if ($this->read !== $this->size) { |
108
|
|
|
$this->reportInvalidShapeTypeError(); |
109
|
|
|
} |
110
|
|
|
|
111
|
88 |
|
if (! ShapeFile::supportsDbase() || $dbfFile === false) { |
112
|
22 |
|
return; |
113
|
|
|
} |
114
|
|
|
|
115
|
66 |
|
$this->loadDBFData($dbfFile); |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Saves record to files. |
120
|
|
|
* |
121
|
|
|
* @param resource $shpFile Opened SHP file |
122
|
|
|
* @param resource|false $dbfFile Opened DBF file |
123
|
|
|
* @param int $recordNumber Record number |
124
|
|
|
*/ |
125
|
57 |
|
public function saveToFile($shpFile, $dbfFile, int $recordNumber): void |
126
|
|
|
{ |
127
|
57 |
|
$this->shpFile = $shpFile; |
128
|
57 |
|
$this->recordNumber = $recordNumber; |
129
|
57 |
|
$this->saveHeaders(); |
130
|
|
|
|
131
|
57 |
|
match ($this->shapeType) { |
132
|
57 |
|
ShapeType::Null => null, // Nothing to save |
133
|
57 |
|
ShapeType::Point => $this->savePointRecord(), |
|
|
|
|
134
|
53 |
|
ShapeType::PointM => $this->savePointMRecord(), |
|
|
|
|
135
|
49 |
|
ShapeType::PointZ => $this->savePointZRecord(), |
|
|
|
|
136
|
45 |
|
ShapeType::PolyLine => $this->savePolyLineRecord(), |
|
|
|
|
137
|
32 |
|
ShapeType::PolyLineM => $this->savePolyLineMRecord(), |
|
|
|
|
138
|
28 |
|
ShapeType::PolyLineZ => $this->savePolyLineZRecord(), |
|
|
|
|
139
|
24 |
|
ShapeType::Polygon => $this->savePolygonRecord(), |
|
|
|
|
140
|
20 |
|
ShapeType::PolygonM => $this->savePolygonMRecord(), |
|
|
|
|
141
|
16 |
|
ShapeType::PolygonZ => $this->savePolygonZRecord(), |
|
|
|
|
142
|
12 |
|
ShapeType::MultiPoint => $this->saveMultiPointRecord(), |
|
|
|
|
143
|
8 |
|
ShapeType::MultiPointM => $this->saveMultiPointMRecord(), |
|
|
|
|
144
|
4 |
|
ShapeType::MultiPointZ => $this->saveMultiPointZRecord(), |
|
|
|
|
145
|
|
|
default => $this->reportInvalidShapeTypeError(), |
|
|
|
|
146
|
57 |
|
}; |
147
|
|
|
|
148
|
57 |
|
if (! ShapeFile::supportsDbase() || $dbfFile === false) { |
149
|
12 |
|
return; |
150
|
|
|
} |
151
|
|
|
|
152
|
45 |
|
$this->saveDBFData($dbfFile); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Updates DBF data to match header. |
157
|
|
|
* |
158
|
|
|
* @param mixed[] $header DBF structure header |
159
|
|
|
*/ |
160
|
57 |
|
public function updateDBFInfo(array $header): void |
161
|
|
|
{ |
162
|
57 |
|
$tmp = $this->dbfData; |
163
|
57 |
|
$this->dbfData = []; |
164
|
57 |
|
foreach ($header as [$value]) { |
165
|
57 |
|
$this->dbfData[$value] = $tmp[$value] ?? ''; |
166
|
|
|
} |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* Reads data. |
171
|
|
|
* |
172
|
|
|
* @param string $type type for unpack() |
173
|
|
|
* @param int<0, max> $count number of bytes |
174
|
|
|
*/ |
175
|
88 |
|
private function loadData(string $type, int $count): mixed |
176
|
|
|
{ |
177
|
88 |
|
$data = $this->shapeFile->readSHP($count); |
|
|
|
|
178
|
88 |
|
if ($data === false) { |
179
|
|
|
return false; |
180
|
|
|
} |
181
|
|
|
|
182
|
88 |
|
$this->read += strlen($data); |
183
|
|
|
|
184
|
88 |
|
return Util::loadData($type, $data); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Loads metadata header from a file. |
189
|
|
|
*/ |
190
|
88 |
|
private function loadHeaders(): void |
191
|
|
|
{ |
192
|
88 |
|
$this->shapeType = ShapeType::Unknown; |
193
|
88 |
|
$recordNumber = $this->loadData('N', 4); |
194
|
88 |
|
if ($recordNumber === false) { |
195
|
88 |
|
return; |
196
|
|
|
} |
197
|
|
|
|
198
|
88 |
|
$this->recordNumber = (int) $recordNumber; |
199
|
|
|
|
200
|
|
|
// We read the length of the record |
201
|
88 |
|
$size = $this->loadData('N', 4); |
202
|
88 |
|
if ($size === false) { |
203
|
|
|
return; |
204
|
|
|
} |
205
|
|
|
|
206
|
88 |
|
$this->size = ($size * 2) + 8; |
207
|
|
|
|
208
|
88 |
|
$shapeType = $this->loadData('V', 4); |
209
|
88 |
|
if ($shapeType === false) { |
210
|
|
|
return; |
211
|
|
|
} |
212
|
|
|
|
213
|
88 |
|
$this->shapeType = ShapeType::tryFrom((int) $shapeType) ?? ShapeType::Unknown; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* Saves metadata header to a file. |
218
|
|
|
*/ |
219
|
57 |
|
private function saveHeaders(): void |
220
|
|
|
{ |
221
|
57 |
|
fwrite($this->shpFile, pack('N', $this->recordNumber)); |
222
|
57 |
|
fwrite($this->shpFile, pack('N', $this->getContentLength())); |
223
|
57 |
|
fwrite($this->shpFile, pack('V', $this->shapeType->value)); |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** @return mixed[] */ |
227
|
88 |
|
private function loadPoint(): array |
228
|
|
|
{ |
229
|
88 |
|
return [ |
230
|
88 |
|
'x' => $this->loadData('d', 8), |
231
|
88 |
|
'y' => $this->loadData('d', 8), |
232
|
88 |
|
]; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** @return mixed[] */ |
236
|
13 |
|
private function loadPointM(): array |
237
|
|
|
{ |
238
|
13 |
|
$data = $this->loadPoint(); |
239
|
|
|
|
240
|
13 |
|
$data['m'] = $this->loadData('d', 8); |
241
|
|
|
|
242
|
13 |
|
return $data; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** @return mixed[] */ |
246
|
13 |
|
private function loadPointZ(): array |
247
|
|
|
{ |
248
|
13 |
|
$data = $this->loadPoint(); |
249
|
|
|
|
250
|
13 |
|
$data['z'] = $this->loadData('d', 8); |
251
|
13 |
|
$data['m'] = $this->loadData('d', 8); |
252
|
|
|
|
253
|
13 |
|
return $data; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** @param mixed[] $data */ |
257
|
49 |
|
private function savePoint(array $data): void |
258
|
|
|
{ |
259
|
49 |
|
fwrite($this->shpFile, Util::packDouble($data['x'])); |
260
|
49 |
|
fwrite($this->shpFile, Util::packDouble($data['y'])); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** @param mixed[] $data */ |
264
|
13 |
|
private function savePointM(array $data): void |
265
|
|
|
{ |
266
|
13 |
|
fwrite($this->shpFile, Util::packDouble($data['x'])); |
267
|
13 |
|
fwrite($this->shpFile, Util::packDouble($data['y'])); |
268
|
13 |
|
fwrite($this->shpFile, Util::packDouble($data['m'])); |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** @param mixed[] $data */ |
272
|
13 |
|
private function savePointZ(array $data): void |
273
|
|
|
{ |
274
|
13 |
|
fwrite($this->shpFile, Util::packDouble($data['x'])); |
275
|
13 |
|
fwrite($this->shpFile, Util::packDouble($data['y'])); |
276
|
13 |
|
fwrite($this->shpFile, Util::packDouble($data['z'])); |
277
|
13 |
|
fwrite($this->shpFile, Util::packDouble($data['m'])); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
private function loadNullRecord(): void |
281
|
|
|
{ |
282
|
|
|
$this->shpData = []; |
283
|
|
|
} |
284
|
|
|
|
285
|
25 |
|
private function loadPointRecord(): void |
286
|
|
|
{ |
287
|
25 |
|
$this->shpData = $this->loadPoint(); |
288
|
|
|
} |
289
|
|
|
|
290
|
13 |
|
private function loadPointMRecord(): void |
291
|
|
|
{ |
292
|
13 |
|
$this->shpData = $this->loadPointM(); |
293
|
|
|
} |
294
|
|
|
|
295
|
13 |
|
private function loadPointZRecord(): void |
296
|
|
|
{ |
297
|
13 |
|
$this->shpData = $this->loadPointZ(); |
298
|
|
|
} |
299
|
|
|
|
300
|
13 |
|
private function savePointRecord(): void |
301
|
|
|
{ |
302
|
13 |
|
$this->savePoint($this->shpData); |
303
|
|
|
} |
304
|
|
|
|
305
|
13 |
|
private function savePointMRecord(): void |
306
|
|
|
{ |
307
|
13 |
|
$this->savePointM($this->shpData); |
308
|
|
|
} |
309
|
|
|
|
310
|
13 |
|
private function savePointZRecord(): void |
311
|
|
|
{ |
312
|
13 |
|
$this->savePointZ($this->shpData); |
313
|
|
|
} |
314
|
|
|
|
315
|
64 |
|
private function loadBBox(): void |
316
|
|
|
{ |
317
|
64 |
|
$this->shpData['xmin'] = $this->loadData('d', 8); |
318
|
64 |
|
$this->shpData['ymin'] = $this->loadData('d', 8); |
319
|
64 |
|
$this->shpData['xmax'] = $this->loadData('d', 8); |
320
|
64 |
|
$this->shpData['ymax'] = $this->loadData('d', 8); |
321
|
|
|
} |
322
|
|
|
|
323
|
16 |
|
private function loadMultiPointRecord(): void |
324
|
|
|
{ |
325
|
16 |
|
$this->shpData = []; |
326
|
16 |
|
$this->loadBBox(); |
327
|
|
|
|
328
|
16 |
|
$this->shpData['numpoints'] = $this->loadData('V', 4); |
329
|
|
|
|
330
|
16 |
|
for ($i = 0; $i < $this->shpData['numpoints']; ++$i) { |
331
|
16 |
|
$this->shpData['points'][] = $this->loadPoint(); |
332
|
|
|
} |
333
|
|
|
} |
334
|
|
|
|
335
|
12 |
|
private function loadMultiPointMZRecord(string $type): void |
336
|
|
|
{ |
337
|
|
|
/* The m dimension is optional, depends on bounding box data */ |
338
|
12 |
|
if ($type === 'm' && ! $this->shapeFile->hasMeasure()) { |
339
|
12 |
|
return; |
340
|
|
|
} |
341
|
|
|
|
342
|
8 |
|
$this->shpData[$type . 'min'] = $this->loadData('d', 8); |
343
|
8 |
|
$this->shpData[$type . 'max'] = $this->loadData('d', 8); |
344
|
|
|
|
345
|
8 |
|
for ($i = 0; $i < $this->shpData['numpoints']; ++$i) { |
346
|
8 |
|
$this->shpData['points'][$i][$type] = $this->loadData('d', 8); |
347
|
|
|
} |
348
|
|
|
} |
349
|
|
|
|
350
|
4 |
|
private function loadMultiPointMRecord(): void |
351
|
|
|
{ |
352
|
4 |
|
$this->loadMultiPointRecord(); |
353
|
|
|
|
354
|
4 |
|
$this->loadMultiPointMZRecord('m'); |
355
|
|
|
} |
356
|
|
|
|
357
|
8 |
|
private function loadMultiPointZRecord(): void |
358
|
|
|
{ |
359
|
8 |
|
$this->loadMultiPointRecord(); |
360
|
|
|
|
361
|
8 |
|
$this->loadMultiPointMZRecord('z'); |
362
|
8 |
|
$this->loadMultiPointMZRecord('m'); |
363
|
|
|
} |
364
|
|
|
|
365
|
12 |
|
private function saveMultiPointRecord(): void |
366
|
|
|
{ |
367
|
12 |
|
fwrite($this->shpFile, pack( |
368
|
12 |
|
'dddd', |
369
|
12 |
|
$this->shpData['xmin'], |
370
|
12 |
|
$this->shpData['ymin'], |
371
|
12 |
|
$this->shpData['xmax'], |
372
|
12 |
|
$this->shpData['ymax'], |
373
|
12 |
|
)); |
374
|
|
|
|
375
|
12 |
|
fwrite($this->shpFile, pack('V', $this->shpData['numpoints'])); |
376
|
|
|
|
377
|
12 |
|
for ($i = 0; $i < $this->shpData['numpoints']; ++$i) { |
378
|
12 |
|
$this->savePoint($this->shpData['points'][$i]); |
379
|
|
|
} |
380
|
|
|
} |
381
|
|
|
|
382
|
8 |
|
private function saveMultiPointMZRecord(string $type): void |
383
|
|
|
{ |
384
|
8 |
|
fwrite($this->shpFile, pack('dd', $this->shpData[$type . 'min'], $this->shpData[$type . 'max'])); |
385
|
|
|
|
386
|
8 |
|
for ($i = 0; $i < $this->shpData['numpoints']; ++$i) { |
387
|
8 |
|
fwrite($this->shpFile, Util::packDouble($this->shpData['points'][$i][$type])); |
388
|
|
|
} |
389
|
|
|
} |
390
|
|
|
|
391
|
4 |
|
private function saveMultiPointMRecord(): void |
392
|
|
|
{ |
393
|
4 |
|
$this->saveMultiPointRecord(); |
394
|
|
|
|
395
|
4 |
|
$this->saveMultiPointMZRecord('m'); |
396
|
|
|
} |
397
|
|
|
|
398
|
4 |
|
private function saveMultiPointZRecord(): void |
399
|
|
|
{ |
400
|
4 |
|
$this->saveMultiPointRecord(); |
401
|
|
|
|
402
|
4 |
|
$this->saveMultiPointMZRecord('z'); |
403
|
4 |
|
$this->saveMultiPointMZRecord('m'); |
404
|
|
|
} |
405
|
|
|
|
406
|
48 |
|
private function loadPolyLineRecord(): void |
407
|
|
|
{ |
408
|
48 |
|
$this->shpData = []; |
409
|
48 |
|
$this->loadBBox(); |
410
|
|
|
|
411
|
48 |
|
$this->shpData['numparts'] = $this->loadData('V', 4); |
412
|
48 |
|
$this->shpData['numpoints'] = $this->loadData('V', 4); |
413
|
|
|
|
414
|
48 |
|
$numparts = $this->shpData['numparts']; |
415
|
48 |
|
$numpoints = $this->shpData['numpoints']; |
416
|
|
|
|
417
|
48 |
|
for ($i = 0; $i < $numparts; ++$i) { |
418
|
48 |
|
$this->shpData['parts'][$i] = $this->loadData('V', 4); |
419
|
|
|
} |
420
|
|
|
|
421
|
48 |
|
$part = 0; |
422
|
48 |
|
for ($i = 0; $i < $numpoints; ++$i) { |
423
|
48 |
|
if ($part + 1 < $numparts && $i === $this->shpData['parts'][$part + 1]) { |
424
|
44 |
|
++$part; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
if ( |
428
|
48 |
|
! isset($this->shpData['parts'][$part]['points']) |
429
|
48 |
|
|| ! is_array($this->shpData['parts'][$part]['points']) |
430
|
|
|
) { |
431
|
48 |
|
$this->shpData['parts'][$part] = ['points' => []]; |
432
|
|
|
} |
433
|
|
|
|
434
|
48 |
|
$this->shpData['parts'][$part]['points'][] = $this->loadPoint(); |
435
|
|
|
} |
436
|
|
|
} |
437
|
|
|
|
438
|
20 |
|
private function loadPolyLineMZRecord(string $type): void |
439
|
|
|
{ |
440
|
|
|
/* The m dimension is optional, depends on bounding box data */ |
441
|
20 |
|
if ($type === 'm' && ! $this->shapeFile->hasMeasure()) { |
442
|
20 |
|
return; |
443
|
|
|
} |
444
|
|
|
|
445
|
12 |
|
$this->shpData[$type . 'min'] = $this->loadData('d', 8); |
446
|
12 |
|
$this->shpData[$type . 'max'] = $this->loadData('d', 8); |
447
|
|
|
|
448
|
12 |
|
$numparts = $this->shpData['numparts']; |
449
|
12 |
|
$numpoints = $this->shpData['numpoints']; |
450
|
|
|
|
451
|
12 |
|
$part = 0; |
452
|
12 |
|
for ($i = 0; $i < $numpoints; ++$i) { |
453
|
12 |
|
if ($part + 1 < $numparts && $i === $this->shpData['parts'][$part + 1]) { |
454
|
|
|
++$part; |
455
|
|
|
} |
456
|
|
|
|
457
|
12 |
|
$this->shpData['parts'][$part]['points'][$i][$type] = $this->loadData('d', 8); |
458
|
|
|
} |
459
|
|
|
} |
460
|
|
|
|
461
|
8 |
|
private function loadPolyLineMRecord(): void |
462
|
|
|
{ |
463
|
8 |
|
$this->loadPolyLineRecord(); |
464
|
|
|
|
465
|
8 |
|
$this->loadPolyLineMZRecord('m'); |
466
|
|
|
} |
467
|
|
|
|
468
|
12 |
|
private function loadPolyLineZRecord(): void |
469
|
|
|
{ |
470
|
12 |
|
$this->loadPolyLineRecord(); |
471
|
|
|
|
472
|
12 |
|
$this->loadPolyLineMZRecord('z'); |
473
|
12 |
|
$this->loadPolyLineMZRecord('m'); |
474
|
|
|
} |
475
|
|
|
|
476
|
33 |
|
private function savePolyLineRecord(): void |
477
|
|
|
{ |
478
|
33 |
|
fwrite($this->shpFile, pack( |
479
|
33 |
|
'dddd', |
480
|
33 |
|
$this->shpData['xmin'], |
481
|
33 |
|
$this->shpData['ymin'], |
482
|
33 |
|
$this->shpData['xmax'], |
483
|
33 |
|
$this->shpData['ymax'], |
484
|
33 |
|
)); |
485
|
|
|
|
486
|
33 |
|
fwrite($this->shpFile, pack('VV', $this->shpData['numparts'], $this->shpData['numpoints'])); |
487
|
|
|
|
488
|
33 |
|
$partIndex = 0; |
489
|
33 |
|
for ($i = 0; $i < $this->shpData['numparts']; ++$i) { |
490
|
33 |
|
fwrite($this->shpFile, pack('V', $partIndex)); |
491
|
33 |
|
$partIndex += count($this->shpData['parts'][$i]['points']); |
492
|
|
|
} |
493
|
|
|
|
494
|
33 |
|
foreach ($this->shpData['parts'] as $partData) { |
495
|
33 |
|
foreach ($partData['points'] as $pointData) { |
496
|
33 |
|
$this->savePoint($pointData); |
497
|
|
|
} |
498
|
|
|
} |
499
|
|
|
} |
500
|
|
|
|
501
|
16 |
|
private function savePolyLineMZRecord(string $type): void |
502
|
|
|
{ |
503
|
16 |
|
fwrite($this->shpFile, pack('dd', $this->shpData[$type . 'min'], $this->shpData[$type . 'max'])); |
504
|
|
|
|
505
|
16 |
|
foreach ($this->shpData['parts'] as $partData) { |
506
|
16 |
|
foreach ($partData['points'] as $pointData) { |
507
|
16 |
|
fwrite($this->shpFile, Util::packDouble($pointData[$type])); |
508
|
|
|
} |
509
|
|
|
} |
510
|
|
|
} |
511
|
|
|
|
512
|
8 |
|
private function savePolyLineMRecord(): void |
513
|
|
|
{ |
514
|
8 |
|
$this->savePolyLineRecord(); |
515
|
|
|
|
516
|
8 |
|
$this->savePolyLineMZRecord('m'); |
517
|
|
|
} |
518
|
|
|
|
519
|
8 |
|
private function savePolyLineZRecord(): void |
520
|
|
|
{ |
521
|
8 |
|
$this->savePolyLineRecord(); |
522
|
|
|
|
523
|
8 |
|
$this->savePolyLineMZRecord('z'); |
524
|
8 |
|
$this->savePolyLineMZRecord('m'); |
525
|
|
|
} |
526
|
|
|
|
527
|
15 |
|
private function loadPolygonRecord(): void |
528
|
|
|
{ |
529
|
15 |
|
$this->loadPolyLineRecord(); |
530
|
|
|
} |
531
|
|
|
|
532
|
4 |
|
private function loadPolygonMRecord(): void |
533
|
|
|
{ |
534
|
4 |
|
$this->loadPolyLineMRecord(); |
535
|
|
|
} |
536
|
|
|
|
537
|
8 |
|
private function loadPolygonZRecord(): void |
538
|
|
|
{ |
539
|
8 |
|
$this->loadPolyLineZRecord(); |
540
|
|
|
} |
541
|
|
|
|
542
|
4 |
|
private function savePolygonRecord(): void |
543
|
|
|
{ |
544
|
4 |
|
$this->savePolyLineRecord(); |
545
|
|
|
} |
546
|
|
|
|
547
|
4 |
|
private function savePolygonMRecord(): void |
548
|
|
|
{ |
549
|
4 |
|
$this->savePolyLineMRecord(); |
550
|
|
|
} |
551
|
|
|
|
552
|
4 |
|
private function savePolygonZRecord(): void |
553
|
|
|
{ |
554
|
4 |
|
$this->savePolyLineZRecord(); |
555
|
|
|
} |
556
|
|
|
|
557
|
|
|
/** @param mixed[] $point */ |
558
|
57 |
|
private function adjustBBox(array $point): void |
559
|
|
|
{ |
560
|
|
|
// Adjusts bounding box based on point |
561
|
57 |
|
foreach (['x', 'y', 'z', 'm'] as $direction) { |
562
|
57 |
|
if (! isset($point[$direction])) { |
563
|
41 |
|
continue; |
564
|
|
|
} |
565
|
|
|
|
566
|
57 |
|
$min = $direction . 'min'; |
567
|
57 |
|
$max = $direction . 'max'; |
568
|
57 |
|
if (! isset($this->shpData[$min]) || ($this->shpData[$min] > $point[$direction])) { |
569
|
57 |
|
$this->shpData[$min] = $point[$direction]; |
570
|
|
|
} |
571
|
|
|
|
572
|
57 |
|
if (isset($this->shpData[$max]) && ($this->shpData[$max] >= $point[$direction])) { |
573
|
45 |
|
continue; |
574
|
|
|
} |
575
|
|
|
|
576
|
57 |
|
$this->shpData[$max] = $point[$direction]; |
577
|
|
|
} |
578
|
|
|
} |
579
|
|
|
|
580
|
|
|
/** |
581
|
|
|
* Adjust point and bounding box when adding point. |
582
|
|
|
* Sets dimension to 0 if not set. |
583
|
|
|
* |
584
|
|
|
* @param mixed[] $point Point data |
585
|
|
|
* |
586
|
|
|
* @return mixed[] Fixed point data |
587
|
|
|
*/ |
588
|
57 |
|
private function adjustPoint(array $point): array |
589
|
|
|
{ |
590
|
57 |
|
if (in_array($this->shapeType, ShapeType::MEASURED_TYPES, true)) { |
|
|
|
|
591
|
41 |
|
$point['m'] ??= 0.0; |
592
|
|
|
} |
593
|
|
|
|
594
|
57 |
|
if (in_array($this->shapeType, ShapeType::TYPES_WITH_Z, true)) { |
|
|
|
|
595
|
25 |
|
$point['z'] ??= 0.0; |
596
|
|
|
} |
597
|
|
|
|
598
|
57 |
|
return $point; |
599
|
|
|
} |
600
|
|
|
|
601
|
|
|
/** |
602
|
|
|
* Adds point to a record. |
603
|
|
|
* |
604
|
|
|
* @param mixed[] $point Point data |
605
|
|
|
* @param int $partIndex Part index |
606
|
|
|
*/ |
607
|
57 |
|
public function addPoint(array $point, int $partIndex = 0): void |
608
|
|
|
{ |
609
|
57 |
|
$point = $this->adjustPoint($point); |
610
|
57 |
|
switch ($this->shapeType) { |
611
|
57 |
|
case ShapeType::Null: |
612
|
|
|
//Don't add anything |
613
|
|
|
return; |
614
|
|
|
|
615
|
57 |
|
case ShapeType::Point: |
616
|
53 |
|
case ShapeType::PointZ: |
617
|
49 |
|
case ShapeType::PointM: |
618
|
|
|
//Substitutes the value of the current point |
619
|
21 |
|
$this->shpData = $point; |
620
|
21 |
|
break; |
621
|
45 |
|
case ShapeType::PolyLine: |
622
|
32 |
|
case ShapeType::Polygon: |
623
|
28 |
|
case ShapeType::PolyLineZ: |
624
|
24 |
|
case ShapeType::PolygonZ: |
625
|
20 |
|
case ShapeType::PolyLineM: |
626
|
16 |
|
case ShapeType::PolygonM: |
627
|
|
|
//Adds a new point to the selected part |
628
|
33 |
|
$this->shpData['parts'][$partIndex]['points'][] = $point; |
629
|
33 |
|
$this->shpData['numparts'] = count($this->shpData['parts']); |
630
|
33 |
|
$this->shpData['numpoints'] = 1 + ($this->shpData['numpoints'] ?? 0); |
631
|
33 |
|
break; |
632
|
12 |
|
case ShapeType::MultiPoint: |
633
|
8 |
|
case ShapeType::MultiPointZ: |
634
|
4 |
|
case ShapeType::MultiPointM: |
635
|
|
|
//Adds a new point |
636
|
12 |
|
$this->shpData['points'][] = $point; |
637
|
12 |
|
$this->shpData['numpoints'] = 1 + ($this->shpData['numpoints'] ?? 0); |
638
|
12 |
|
break; |
639
|
|
|
default: |
640
|
|
|
$this->reportInvalidShapeTypeError(); |
641
|
|
|
|
642
|
|
|
return; |
643
|
|
|
} |
644
|
|
|
|
645
|
57 |
|
$this->adjustBBox($point); |
646
|
|
|
} |
647
|
|
|
|
648
|
|
|
/** |
649
|
|
|
* Deletes point from a record. |
650
|
|
|
* |
651
|
|
|
* @param int $pointIndex Point index |
652
|
|
|
* @param int $partIndex Part index |
653
|
|
|
*/ |
654
|
48 |
|
public function deletePoint(int $pointIndex = 0, int $partIndex = 0): void |
655
|
|
|
{ |
656
|
48 |
|
switch ($this->shapeType) { |
657
|
48 |
|
case ShapeType::Null: |
658
|
|
|
//Don't delete anything |
659
|
|
|
break; |
660
|
48 |
|
case ShapeType::Point: |
661
|
44 |
|
case ShapeType::PointZ: |
662
|
40 |
|
case ShapeType::PointM: |
663
|
|
|
//Sets the value of the point to zero |
664
|
12 |
|
$this->shpData['x'] = 0.0; |
665
|
12 |
|
$this->shpData['y'] = 0.0; |
666
|
12 |
|
if (in_array($this->shapeType, [ShapeType::PointZ, ShapeType::PointM], true)) { |
667
|
8 |
|
$this->shpData['m'] = 0.0; |
668
|
|
|
} |
669
|
|
|
|
670
|
12 |
|
if ($this->shapeType === ShapeType::PointZ) { |
671
|
4 |
|
$this->shpData['z'] = 0.0; |
672
|
|
|
} |
673
|
|
|
|
674
|
12 |
|
break; |
675
|
36 |
|
case ShapeType::PolyLine: |
676
|
32 |
|
case ShapeType::Polygon: |
677
|
28 |
|
case ShapeType::PolyLineZ: |
678
|
24 |
|
case ShapeType::PolygonZ: |
679
|
20 |
|
case ShapeType::PolyLineM: |
680
|
16 |
|
case ShapeType::PolygonM: |
681
|
|
|
//Deletes the point from the selected part, if exists |
682
|
|
|
if ( |
683
|
24 |
|
isset($this->shpData['parts'][$partIndex]) |
684
|
24 |
|
&& isset($this->shpData['parts'][$partIndex]['points'][$pointIndex]) |
685
|
|
|
) { |
686
|
24 |
|
$count = count($this->shpData['parts'][$partIndex]['points']) - 1; |
687
|
24 |
|
for ($i = $pointIndex; $i < $count; ++$i) { |
688
|
24 |
|
$point = $this->shpData['parts'][$partIndex]['points'][$i + 1]; |
689
|
24 |
|
$this->shpData['parts'][$partIndex]['points'][$i] = $point; |
690
|
|
|
} |
691
|
|
|
|
692
|
24 |
|
$count = count($this->shpData['parts'][$partIndex]['points']) - 1; |
693
|
24 |
|
unset($this->shpData['parts'][$partIndex]['points'][$count]); |
694
|
|
|
|
695
|
24 |
|
$this->shpData['numparts'] = count($this->shpData['parts']); |
696
|
24 |
|
--$this->shpData['numpoints']; |
697
|
|
|
} |
698
|
|
|
|
699
|
24 |
|
break; |
700
|
12 |
|
case ShapeType::MultiPoint: |
701
|
8 |
|
case ShapeType::MultiPointZ: |
702
|
4 |
|
case ShapeType::MultiPointM: |
703
|
|
|
//Deletes the point, if exists |
704
|
12 |
|
if (isset($this->shpData['points'][$pointIndex])) { |
705
|
12 |
|
$count = count($this->shpData['points']) - 1; |
706
|
12 |
|
for ($i = $pointIndex; $i < $count; ++$i) { |
707
|
12 |
|
$this->shpData['points'][$i] = $this->shpData['points'][$i + 1]; |
708
|
|
|
} |
709
|
|
|
|
710
|
12 |
|
unset($this->shpData['points'][count($this->shpData['points']) - 1]); |
711
|
|
|
|
712
|
12 |
|
--$this->shpData['numpoints']; |
713
|
|
|
} |
714
|
|
|
|
715
|
12 |
|
break; |
716
|
|
|
default: |
717
|
|
|
$this->reportInvalidShapeTypeError(); |
718
|
|
|
break; |
719
|
|
|
} |
720
|
|
|
} |
721
|
|
|
|
722
|
|
|
/** |
723
|
|
|
* Returns length of content. |
724
|
|
|
*/ |
725
|
57 |
|
public function getContentLength(): int|null |
726
|
|
|
{ |
727
|
|
|
// The content length for a record is the length of the record contents section measured in 16-bit words. |
728
|
|
|
// one coordinate makes 4 16-bit words (64 bit double) |
729
|
57 |
|
switch ($this->shapeType) { |
730
|
57 |
|
case ShapeType::Null: |
731
|
|
|
$result = 0; |
732
|
|
|
break; |
733
|
57 |
|
case ShapeType::Point: |
734
|
13 |
|
$result = 10; |
735
|
13 |
|
break; |
736
|
53 |
|
case ShapeType::PointM: |
737
|
13 |
|
$result = 10 + 4; |
738
|
13 |
|
break; |
739
|
49 |
|
case ShapeType::PointZ: |
740
|
13 |
|
$result = 10 + 8; |
741
|
13 |
|
break; |
742
|
45 |
|
case ShapeType::PolyLine: |
743
|
32 |
|
case ShapeType::Polygon: |
744
|
17 |
|
$count = count($this->shpData['parts']); |
745
|
17 |
|
$result = 22 + 2 * $count; |
746
|
17 |
|
for ($i = 0; $i < $count; ++$i) { |
747
|
17 |
|
$result += 8 * count($this->shpData['parts'][$i]['points']); |
748
|
|
|
} |
749
|
|
|
|
750
|
17 |
|
break; |
751
|
28 |
|
case ShapeType::PolyLineM: |
752
|
24 |
|
case ShapeType::PolygonM: |
753
|
8 |
|
$count = count($this->shpData['parts']); |
754
|
8 |
|
$result = 22 + (2 * 4) + 2 * $count; |
755
|
8 |
|
for ($i = 0; $i < $count; ++$i) { |
756
|
8 |
|
$result += (8 + 4) * count($this->shpData['parts'][$i]['points']); |
757
|
|
|
} |
758
|
|
|
|
759
|
8 |
|
break; |
760
|
20 |
|
case ShapeType::PolyLineZ: |
761
|
16 |
|
case ShapeType::PolygonZ: |
762
|
8 |
|
$count = count($this->shpData['parts']); |
763
|
8 |
|
$result = 22 + (4 * 4) + 2 * $count; |
764
|
8 |
|
for ($i = 0; $i < $count; ++$i) { |
765
|
8 |
|
$result += (8 + 8) * count($this->shpData['parts'][$i]['points']); |
766
|
|
|
} |
767
|
|
|
|
768
|
8 |
|
break; |
769
|
12 |
|
case ShapeType::MultiPoint: |
770
|
4 |
|
$result = 20 + 8 * count($this->shpData['points']); |
771
|
4 |
|
break; |
772
|
8 |
|
case ShapeType::MultiPointM: |
773
|
4 |
|
$result = 20 + (2 * 4) + (8 + 4) * count($this->shpData['points']); |
774
|
4 |
|
break; |
775
|
4 |
|
case ShapeType::MultiPointZ: |
776
|
4 |
|
$result = 20 + (4 * 4) + (8 + 8) * count($this->shpData['points']); |
777
|
4 |
|
break; |
778
|
|
|
default: |
779
|
|
|
$result = null; |
780
|
|
|
$this->reportInvalidShapeTypeError(); |
781
|
|
|
break; |
782
|
|
|
} |
783
|
|
|
|
784
|
57 |
|
return $result; |
785
|
|
|
} |
786
|
|
|
|
787
|
|
|
/** @param resource $dbfFile Opened DBF file */ |
788
|
66 |
|
private function loadDBFData($dbfFile): void |
789
|
|
|
{ |
790
|
66 |
|
$this->dbfData = @dbase_get_record_with_names($dbfFile, $this->recordNumber); |
|
|
|
|
791
|
66 |
|
unset($this->dbfData['deleted']); |
792
|
|
|
} |
793
|
|
|
|
794
|
|
|
/** @param resource $dbfFile */ |
795
|
45 |
|
private function saveDBFData($dbfFile): void |
796
|
|
|
{ |
797
|
45 |
|
if ($this->dbfData === []) { |
798
|
|
|
return; |
799
|
|
|
} |
800
|
|
|
|
801
|
45 |
|
unset($this->dbfData['deleted']); |
802
|
45 |
|
if ($this->recordNumber <= dbase_numrecords($dbfFile)) { |
|
|
|
|
803
|
|
|
if (! dbase_replace_record($dbfFile, array_values($this->dbfData), $this->recordNumber)) { |
|
|
|
|
804
|
|
|
$this->setError("I wasn't possible to update the information in the DBF file."); |
805
|
|
|
} |
806
|
45 |
|
} elseif (! dbase_add_record($dbfFile, array_values($this->dbfData))) { |
|
|
|
|
807
|
|
|
$this->setError("I wasn't possible to add the information to the DBF file."); |
808
|
|
|
} |
809
|
|
|
} |
810
|
|
|
|
811
|
|
|
/** |
812
|
|
|
* Sets error message. |
813
|
|
|
*/ |
814
|
|
|
public function setError(string $error): void |
815
|
|
|
{ |
816
|
|
|
$this->lastError = $error; |
817
|
|
|
} |
818
|
|
|
|
819
|
|
|
/** |
820
|
|
|
* Returns shape name. |
821
|
|
|
* |
822
|
|
|
* @psalm-return non-empty-string |
823
|
|
|
*/ |
824
|
4 |
|
public function getShapeName(): string |
825
|
|
|
{ |
826
|
4 |
|
return ShapeType::name($this->shapeType); |
827
|
|
|
} |
828
|
|
|
|
829
|
|
|
private function reportInvalidShapeTypeError(): void |
830
|
|
|
{ |
831
|
|
|
$this->setError('Invalid Shape type.'); |
832
|
|
|
} |
833
|
|
|
} |
834
|
|
|
|