melder/vendor/Shapefile/ShapefileReader.php

958 lines
33 KiB
PHP
Raw Permalink Normal View History

2024-02-16 15:35:01 +01:00
<?php
/**
* PHP Shapefile - PHP library to read and write ESRI Shapefiles, compatible with WKT and GeoJSON
*
* @package Shapefile
* @author Gaspare Sganga
* @version 3.3.0
* @license MIT
* @link https://gasparesganga.com/labs/php-shapefile/
*/
namespace Shapefile;
use Shapefile\Geometry\Point;
use Shapefile\Geometry\MultiPoint;
use Shapefile\Geometry\Linestring;
use Shapefile\Geometry\MultiLinestring;
use Shapefile\Geometry\Polygon;
use Shapefile\Geometry\MultiPolygon;
/**
* ShapefileReader class.
*/
class ShapefileReader extends Shapefile implements \Iterator
{
/** SHP read methods hash */
private static $shp_read_methods = [
Shapefile::SHAPE_TYPE_NULL => 'readNull',
Shapefile::SHAPE_TYPE_POINT => 'readPoint',
Shapefile::SHAPE_TYPE_POLYLINE => 'readPolyLine',
Shapefile::SHAPE_TYPE_POLYGON => 'readPolygon',
Shapefile::SHAPE_TYPE_MULTIPOINT => 'readMultiPoint',
Shapefile::SHAPE_TYPE_POINTZ => 'readPointZ',
Shapefile::SHAPE_TYPE_POLYLINEZ => 'readPolyLineZ',
Shapefile::SHAPE_TYPE_POLYGONZ => 'readPolygonZ',
Shapefile::SHAPE_TYPE_MULTIPOINTZ => 'readMultiPointZ',
Shapefile::SHAPE_TYPE_POINTM => 'readPointM',
Shapefile::SHAPE_TYPE_POLYLINEM => 'readPolyLineM',
Shapefile::SHAPE_TYPE_POLYGONM => 'readPolygonM',
Shapefile::SHAPE_TYPE_MULTIPOINTM => 'readMultiPointM',
];
/**
* @var array DBF field names map: fields are numerically indexed into DBF files.
*/
private $dbf_fields = [];
/**
* @var int DBF file size in bytes.
*/
private $dbf_file_size;
/**
* @var int DBF file header size in bytes.
*/
private $dbf_header_size;
/**
* @var int DBF file record size in bytes.
*/
private $dbf_record_size;
/**
* @var int DBT file size in bytes.
*/
private $dbt_file_size;
/**
* @var int Pointer to current SHP and DBF files record.
*/
private $current_record;
/////////////////////////////// PUBLIC ///////////////////////////////
/**
* Constructor.
*
* @param string|array $files Path to SHP file / Array of paths / Array of handles of individual files.
* @param array $options Optional associative array of options.
*/
public function __construct($files, $options = [])
{
// Deprecated options
if (isset($options[Shapefile::OPTION_ENFORCE_POLYGON_CLOSED_RINGS])) {
$options = array_merge([
Shapefile::OPTION_POLYGON_CLOSED_RINGS_ACTION => $options[Shapefile::OPTION_ENFORCE_POLYGON_CLOSED_RINGS] ? Shapefile::ACTION_CHECK : Shapefile::ACTION_IGNORE,
], $options);
}
if (isset($options[Shapefile::OPTION_INVERT_POLYGONS_ORIENTATION])) {
$options = array_merge([
Shapefile::OPTION_POLYGON_OUTPUT_ORIENTATION => $options[Shapefile::OPTION_INVERT_POLYGONS_ORIENTATION] ? Shapefile::ORIENTATION_COUNTERCLOCKWISE : Shapefile::ORIENTATION_CLOCKWISE,
], $options);
}
// Options
$this->initOptions([
Shapefile::OPTION_DBF_ALLOW_FIELD_SIZE_255,
Shapefile::OPTION_DBF_CONVERT_TO_UTF8,
Shapefile::OPTION_DBF_FORCE_ALL_CAPS,
Shapefile::OPTION_DBF_IGNORED_FIELDS,
Shapefile::OPTION_DBF_NULL_PADDING_CHAR,
Shapefile::OPTION_DBF_NULLIFY_INVALID_DATES,
Shapefile::OPTION_DBF_RETURN_DATES_AS_OBJECTS,
Shapefile::OPTION_FORCE_MULTIPART_GEOMETRIES,
Shapefile::OPTION_POLYGON_CLOSED_RINGS_ACTION,
Shapefile::OPTION_POLYGON_ORIENTATION_READING_AUTOSENSE,
Shapefile::OPTION_POLYGON_OUTPUT_ORIENTATION,
Shapefile::OPTION_IGNORE_GEOMETRIES_BBOXES,
Shapefile::OPTION_IGNORE_SHAPEFILE_BBOX,
Shapefile::OPTION_SUPPRESS_M,
Shapefile::OPTION_SUPPRESS_Z,
], $options);
// Open files
$this->openFiles($files, false);
// Gets number of records from SHX file size.
$this->setTotRecords(($this->getFileSize(Shapefile::FILE_SHX) - Shapefile::SHX_HEADER_SIZE) / Shapefile::SHX_RECORD_SIZE);
// DBF file size
$this->dbf_file_size = $this->getFileSize(Shapefile::FILE_DBF);
// DBT file size
$this->dbt_file_size = ($this->isFileOpen(Shapefile::FILE_DBT) && $this->getFileSize(Shapefile::FILE_DBT) > 0) ? $this->getFileSize(Shapefile::FILE_DBT) : null;
// PRJ
if ($this->isFileOpen(Shapefile::FILE_PRJ) && $this->getFileSize(Shapefile::FILE_PRJ) > 0) {
$this->setPRJ($this->readString(Shapefile::FILE_PRJ, $this->getFileSize(Shapefile::FILE_PRJ)));
}
// CPG
if ($this->isFileOpen(Shapefile::FILE_CPG) && $this->getFileSize(Shapefile::FILE_CPG) > 0) {
$this->setCharset($this->readString(Shapefile::FILE_CPG, $this->getFileSize(Shapefile::FILE_CPG)));
}
// Read headers
$this->readSHPHeader();
$this->readDBFHeader();
// Init record pointer
$this->rewind();
}
/**
* Destructor.
*
* Closes all files.
*/
public function __destruct()
{
$this->closeFiles();
}
public function rewind()
{
$this->current_record = 0;
$this->next();
}
public function next()
{
++$this->current_record;
if (!$this->checkRecordIndex($this->current_record)) {
$this->current_record = Shapefile::EOF;
}
}
public function current()
{
return $this->readCurrentRecord();
}
public function key()
{
return $this->current_record;
}
public function valid()
{
return ($this->current_record !== Shapefile::EOF);
}
/**
* Gets current record index.
*
* Note that records count starts from 1 in Shapefiles.
* When the last record is reached, the special value Shapefile::EOF will be returned.
*
* @return int
*/
public function getCurrentRecord()
{
return $this->current_record;
}
/**
* Sets current record index. Throws an exception if provided index is out of range.
*
* @param int $index Index of the record to select.
*
* @return self Returns $this to provide a fluent interface.
*/
public function setCurrentRecord($index)
{
if (!$this->checkRecordIndex($index)) {
throw new ShapefileException(Shapefile::ERR_INPUT_RECORD_NOT_FOUND, $index);
}
$this->current_record = $index;
return $this;
}
/**
* Gets current record and moves the cursor to the next one.
*
* @return \Shapefile\Geometry\Geometry
*/
public function fetchRecord()
{
$ret = $this->readCurrentRecord();
if ($ret !== false) {
$this->next();
}
return $ret;
}
/////////////////////////////// PRIVATE ///////////////////////////////
/**
* Reads an unsigned char from a resource handle.
*
* @param string $file_type File type.
*
* @return int
*/
private function readChar($file_type)
{
return current(unpack('C', $this->readData($file_type, 1)));
}
/**
* Reads an unsigned short, 16 bit, little endian byte order, from a resource handle.
*
* @param string $file_type File type.
*
* @return int
*/
private function readInt16L($file_type)
{
return current(unpack('v', $this->readData($file_type, 2)));
}
/**
* Reads an unsigned long, 32 bit, big endian byte order, from a resource handle.
*
* @param string $file_type File type.
*
* @return int
*/
private function readInt32B($file_type)
{
return current(unpack('N', $this->readData($file_type, 4)));
}
/**
* Reads an unsigned long, 32 bit, little endian byte order, from a resource handle.
*
* @param string $file_type File type.
*
* @return int
*/
private function readInt32L($file_type)
{
return current(unpack('V', $this->readData($file_type, 4)));
}
/**
* Reads a double, 64 bit, little endian byte order, from a resource handle.
*
* @param string $file_type File type.
*
* @return double
*/
private function readDoubleL($file_type)
{
$ret = $this->readData($file_type, 8);
if ($this->isBigEndianMachine()) {
$ret = strrev($ret);
}
return current(unpack('d', $ret));
}
/**
* Reads a string of given length from a resource handle and optionally converts it to UTF-8.
*
* @param string $file_type File type.
* @param int $length Length of the string to read.
* @param bool $flag_utf8_encode Optional flag to convert output to UTF-8 if OPTION_DBF_CONVERT_TO_UTF8 is enabled.
*
* @return string
*/
private function readString($file_type, $length, $flag_utf8_encode = false)
{
$ret = current(unpack('A*', $this->readData($file_type, $length)));
if ($flag_utf8_encode && $this->getOption(Shapefile::OPTION_DBF_CONVERT_TO_UTF8)) {
$ret = @iconv($this->getCharset(), 'UTF-8', $ret);
if ($ret === false) {
throw new ShapefileException(Shapefile::ERR_DBF_CHARSET_CONVERSION);
}
}
return trim($ret);
}
/**
* Checks whether a record index value is valid or not.
*
* @param int $index The index value to check.
*
* @return bool
*/
private function checkRecordIndex($index)
{
return ($index > 0 && $index <= $this->getTotRecords());
}
/**
* Reads SHP file header.
*
* @return self Returns $this to provide a fluent interface.
*/
private function readSHPHeader()
{
// Shape Type
$this->setFilePointer(Shapefile::FILE_SHP, 32);
$this->setShapeType($this->readInt32L(Shapefile::FILE_SHP));
// Bounding Box (Z and M ranges are always present in the Shapefile, although with a 0 value if not used)
if (!$this->getOption(Shapefile::OPTION_IGNORE_SHAPEFILE_BBOX)) {
$bounding_box = $this->readXYBoundingBox() + $this->readZRange() + $this->readMRange();
if (!$this->isZ()) {
unset($bounding_box['zmin']);
unset($bounding_box['zmax']);
}
if (!$this->isM()) {
unset($bounding_box['mmin']);
unset($bounding_box['mmax']);
}
$this->setCustomBoundingBox($bounding_box);
}
return $this;
}
/**
* Reads DBF file header.
*
* @return self Returns $this to provide a fluent interface.
*/
private function readDBFHeader()
{
// Number of records
$this->setFilePointer(Shapefile::FILE_DBF, 4);
if ($this->readInt32L(Shapefile::FILE_DBF) !== $this->getTotRecords()) {
throw new ShapefileException(Shapefile::ERR_DBF_MISMATCHED_FILE);
}
// Header and Record size
$this->dbf_header_size = $this->readInt16L(Shapefile::FILE_DBF);
$this->dbf_record_size = $this->readInt16L(Shapefile::FILE_DBF);
// Fields
$this->dbf_fields = [];
$this->setFilePointer(Shapefile::FILE_DBF, 32);
while ($this->getFilePointer(Shapefile::FILE_DBF) < $this->dbf_header_size - 1) {
$name = $this->normalizeDBFFieldNameCase($this->readString(Shapefile::FILE_DBF, 10));
$this->setFileOffset(Shapefile::FILE_DBF, 1);
$type = $this->readString(Shapefile::FILE_DBF, 1);
$this->setFileOffset(Shapefile::FILE_DBF, 4);
$size = $this->readChar(Shapefile::FILE_DBF);
$decimals = $this->readChar(Shapefile::FILE_DBF);
$ignored = in_array($name, $this->getOption(Shapefile::OPTION_DBF_IGNORED_FIELDS));
if ($type === Shapefile::DBF_TYPE_MEMO && !$ignored && !$this->isFileOpen(Shapefile::FILE_DBT)) {
throw new ShapefileException(Shapefile::ERR_FILE_MISSING, strtoupper(Shapefile::FILE_DBT));
}
$this->dbf_fields[] = [
'name' => $ignored ? null : $this->addField($name, $type, $size, $decimals, true),
'ignored' => $ignored,
'size' => $size,
];
$this->setFileOffset(Shapefile::FILE_DBF, 14);
}
// Field terminator byte
if ($this->readChar(Shapefile::FILE_DBF) !== Shapefile::DBF_FIELD_TERMINATOR) {
throw new ShapefileException(Shapefile::ERR_DBF_FILE_NOT_VALID);
}
return $this;
}
/**
* Reads current record in both SHP and DBF files and returns a Geometry.
*
* @return \Shapefile\Geometry\Geometry
*/
private function readCurrentRecord()
{
if (!$this->valid()) {
return false;
}
// === SHX ===
$this->setFilePointer(Shapefile::FILE_SHX, Shapefile::SHX_HEADER_SIZE + (($this->current_record - 1) * Shapefile::SHX_RECORD_SIZE));
// Offset (stored as 16-bit words)
$shp_offset = $this->readInt32B(Shapefile::FILE_SHX) * 2;
// === SHP ===
// Set file pointer position skipping the 8-bytes record header
$this->setFilePointer(Shapefile::FILE_SHP, $shp_offset + 8);
// Shape type
$shape_type = $this->readInt32L(Shapefile::FILE_SHP);
if ($shape_type != Shapefile::SHAPE_TYPE_NULL && $shape_type != $this->getShapeType()) {
throw new ShapefileException(Shapefile::ERR_SHP_WRONG_RECORD_TYPE, $shape_type);
}
// Read Geometry
$Geometry = $this->{self::$shp_read_methods[$shape_type]}();
// === DBF ===
$dbf_file_position = $this->dbf_header_size + (($this->current_record - 1) * $this->dbf_record_size);
// Check if DBF is not corrupted (some "naive" users try to edit the DBF separately...)
// Some GIS do not include the last Shapefile::DBF_EOF_MARKER (0x1a) byte in the DBF file, hence the "- 1" in the following line
if ($dbf_file_position - 1 >= $this->dbf_file_size - $this->dbf_record_size) {
throw new ShapefileException(Shapefile::ERR_DBF_EOF_REACHED);
}
$this->setFilePointer(Shapefile::FILE_DBF, $dbf_file_position);
$Geometry->setFlagDeleted($this->readChar(Shapefile::FILE_DBF) === Shapefile::DBF_DELETED_MARKER);
foreach ($this->dbf_fields as $i => $f) {
if ($f['ignored']) {
$this->setFileOffset(Shapefile::FILE_DBF, $f['size']);
} else {
$type = $this->getField($f['name'])['type'];
$value = $this->decodeFieldValue($f['name'], $type, $this->readString(Shapefile::FILE_DBF, $f['size'], true));
// Memo (DBT)
if ($type === Shapefile::DBF_TYPE_MEMO && $value) {
$this->setFilePointer(Shapefile::FILE_DBT, intval($value) * Shapefile::DBT_BLOCK_SIZE);
$value = '';
do {
if ($this->getFilePointer(Shapefile::FILE_DBT) >= $this->dbt_file_size) {
throw new ShapefileException(Shapefile::ERR_DBT_EOF_REACHED);
}
$value .= $this->readString(Shapefile::FILE_DBT, Shapefile::DBT_BLOCK_SIZE, true);
// Some software only sets ONE field terminator instead of TWO, hence the weird loop condition check:
} while (ord(substr($value, -1)) != Shapefile::DBT_FIELD_TERMINATOR && ord(substr($value, -2, 1)) != Shapefile::DBT_FIELD_TERMINATOR);
$value = substr($value, 0, -2);
}
$Geometry->setData($f['name'], $value);
}
}
$this->pairGeometry($Geometry);
return $Geometry;
}
/**
* Decodes a raw value read from a DBF field.
*
* @param string $field Name of the field.
* @param string $type Type of the field.
* @param string $value Raw value to decode.
*
* @return mixed
*/
private function decodeFieldValue($field, $type, $value)
{
if ($this->getOption(Shapefile::OPTION_DBF_NULL_PADDING_CHAR) !== null && $value == str_repeat($this->getOption(Shapefile::OPTION_DBF_NULL_PADDING_CHAR), $this->getField($field)['size'])) {
$value = null;
} else {
switch ($type) {
case Shapefile::DBF_TYPE_DATE:
$DateTime = \DateTime::createFromFormat('Ymd', $value);
$errors = \DateTime::getLastErrors();
if ($errors['warning_count'] || $errors['error_count']) {
$value = $this->getOption(Shapefile::OPTION_DBF_NULLIFY_INVALID_DATES) ? null : $value;
} elseif ($this->getOption(Shapefile::OPTION_DBF_RETURN_DATES_AS_OBJECTS)) {
$DateTime->setTime(0, 0, 0);
$value = $DateTime;
} else {
$value = $DateTime->format('Y-m-d');
}
break;
case Shapefile::DBF_TYPE_LOGICAL:
$value = ($value === Shapefile::DBF_VALUE_NULL) ? null : strpos(Shapefile::DBF_VALUE_MASK_TRUE, $value) !== false;
break;
}
}
return $value;
}
/**
* Reads an XY pair of coordinates and returns an associative array.
*
* @return array Associative array with "x" and "y" values.
*/
private function readXY()
{
return [
'x' => $this->readDoubleL(Shapefile::FILE_SHP),
'y' => $this->readDoubleL(Shapefile::FILE_SHP),
];
}
/**
* Reads a Z coordinate.
*
* @return array Associative array with "z" value or empty array.
*/
private function readZ()
{
$z = $this->readDoubleL(Shapefile::FILE_SHP);
return $this->getOption(Shapefile::OPTION_SUPPRESS_Z) ? [] : ['z' => $z];
}
/**
* Reads an M coordinate.
*
* @return array Associative array with "m" value or empty array.
*/
private function readM()
{
$m = $this->readDoubleL(Shapefile::FILE_SHP);
return $this->getOption(Shapefile::OPTION_SUPPRESS_M) ? [] : ['m' => $this->parseM($m)];
}
/**
* Parses an M coordinate according to the ESRI specs:
* «Any floating point number smaller than 10^38 is considered by a shapefile reader to represent a "no data" value»
*
* @param float $value Value to parse.
*
* @return float|bool
*/
private function parseM($value)
{
return ($value <= Shapefile::SHP_NO_DATA_THRESHOLD) ? false : $value;
}
/**
* Reads an XY bounding box and returns an associative array.
*
* @return array Associative array with the xmin, xmax, ymin and ymax values.
*/
private function readXYBoundingBox()
{
// Variables are used here because the order of the output array elements is different!
$xmin = $this->readDoubleL(Shapefile::FILE_SHP);
$ymin = $this->readDoubleL(Shapefile::FILE_SHP);
$xmax = $this->readDoubleL(Shapefile::FILE_SHP);
$ymax = $this->readDoubleL(Shapefile::FILE_SHP);
return [
'xmin' => $xmin,
'xmax' => $xmax,
'ymin' => $ymin,
'ymax' => $ymax,
];
}
/**
* Reads a Z range and returns an associative array.
* If flag OPTION_SUPPRESS_Z is set, an empty array will be returned.
*
* @return array Associative array with the zmin and zmax values.
*/
private function readZRange()
{
$values = [
'zmin' => $this->readDoubleL(Shapefile::FILE_SHP),
'zmax' => $this->readDoubleL(Shapefile::FILE_SHP),
];
return $this->getOption(Shapefile::OPTION_SUPPRESS_Z) ? [] : $values;
}
/**
* Reads an M range and returns an associative array.
* If flag OPTION_SUPPRESS_M is set, an empty array will be returned.
*
* @return array Associative array with the mmin and mmax values.
*/
private function readMRange()
{
$values = [
'mmin' => $this->parseM($this->readDoubleL(Shapefile::FILE_SHP)),
'mmax' => $this->parseM($this->readDoubleL(Shapefile::FILE_SHP)),
];
return $this->getOption(Shapefile::OPTION_SUPPRESS_M) ? [] : $values;
}
/**
* Returns an empty Geometry depending on the base type of the Shapefile.
*
* @return \Shapefile\Geometry\Geometry
*/
private function readNull()
{
$geometry_classes = [
Shapefile::SHAPE_TYPE_POINT => 'Point',
Shapefile::SHAPE_TYPE_POLYLINE => 'Linestring',
Shapefile::SHAPE_TYPE_POLYGON => 'Polygon',
Shapefile::SHAPE_TYPE_MULTIPOINT => 'MultiPoint',
];
$shape_basetype = $this->getBasetype();
$geometry_class = $geometry_classes[$shape_basetype];
if ($this->getOption(Shapefile::OPTION_FORCE_MULTIPART_GEOMETRIES) && ($shape_basetype == Shapefile::SHAPE_TYPE_POLYLINE || $shape_basetype == Shapefile::SHAPE_TYPE_POLYGON)) {
$geometry_class = 'Multi' . $geometry_class;
}
$geometry_class = 'Shapefile\Geometry\\' . $geometry_class;
return new $geometry_class();
}
/**
* Reads a Point from the SHP file.
*
* @return \Shapefile\Geometry\Point
*/
private function readPoint()
{
return $this->createPoint($this->readXY());
}
/**
* Reads a PointM from the SHP file.
*
* @return \Shapefile\Geometry\Point
*/
private function readPointM()
{
return $this->createPoint($this->readXY() + $this->readM());
}
/**
* Reads a PointZ from the SHP file.
*
* @return \Shapefile\Geometry\Point
*/
private function readPointZ()
{
return $this->createPoint($this->readXY() + $this->readZ() + $this->readM());
}
/**
* Helper method to create the actual Point Geometry using data read from SHP file.
*
* @param array $data Array with "x", "y" and optional "z" and "m" values.
*
* @return \Shapefile\Geometry\Point
*/
private function createPoint($data)
{
$Geometry = new Point();
$Geometry->initFromArray($data);
return $Geometry;
}
/**
* Reads a MultiPoint from the SHP file.
*
* @param bool $flag_return_geometry Flag to control return type.
*
* @return \Shapefile\Geometry\MultiPoint|array
*/
private function readMultiPoint($flag_return_geometry = true)
{
// Header
$data = [
'bbox' => $this->readXYBoundingBox(),
'geometry' => [
'numpoints' => $this->readInt32L(Shapefile::FILE_SHP),
'points' => [],
],
];
// Points
for ($i = 0; $i < $data['geometry']['numpoints']; ++$i) {
$data['geometry']['points'][] = $this->readXY();
}
return $flag_return_geometry ? $this->createMultiPoint($data) : $data;
}
/**
* Reads a MultiPointM from the SHP file.
*
* @return \Shapefile\Geometry\MultiPoint
*/
private function readMultiPointM()
{
// MultiPoint
$data = $this->readMultiPoint(false);
// M Range
$data['bbox'] += $this->readMRange();
// M Array
for ($i = 0; $i < $data['geometry']['numpoints']; ++$i) {
$data['geometry']['points'][$i] += $this->readM();
}
return $this->createMultiPoint($data);
}
/**
* Reads a MultiPointZ from the SHP file.
*
* @return \Shapefile\Geometry\MultiPoint
*/
private function readMultiPointZ()
{
// MultiPoint
$data = $this->readMultiPoint(false);
// Z Range
$data['bbox'] += $this->readZRange();
// Z Array
for ($i = 0; $i < $data['geometry']['numpoints']; ++$i) {
$data['geometry']['points'][$i] += $this->readZ();
}
// M Range
$data['bbox'] += $this->readMRange();
// M Array
for ($i = 0; $i < $data['geometry']['numpoints']; ++$i) {
$data['geometry']['points'][$i] += $this->readM();
}
return $this->createMultiPoint($data);
}
/**
* Helper method to create the actual MultiPoint Geometry using data read from SHP file.
*
* @param array $data Array with "bbox" and "geometry" values.
*
* @return \Shapefile\Geometry\MultiPoint
*/
private function createMultiPoint($data)
{
$Geometry = new MultiPoint();
$Geometry->initFromArray($data['geometry']);
if (!$this->getOption(Shapefile::OPTION_IGNORE_GEOMETRIES_BBOXES)) {
$Geometry->setCustomBoundingBox($data['bbox']);
}
return $Geometry;
}
/**
* Reads a PolyLine from the SHP file.
*
* @param bool $flag_return_geometry Flag to control return type.
*
* @return \Shapefile\Geometry\Linestring|\Shapefile\Geometry\MultiLinestring|array
*/
private function readPolyLine($flag_return_geometry = true)
{
// Header
$data = [
'bbox' => $this->readXYBoundingBox(),
'geometry' => [
'numparts' => $this->readInt32L(Shapefile::FILE_SHP),
'parts' => [],
],
];
$tot_points = $this->readInt32L(Shapefile::FILE_SHP);
// Parts
$parts_first_index = [];
for ($i = 0; $i < $data['geometry']['numparts']; ++$i) {
$parts_first_index[$i] = $this->readInt32L(Shapefile::FILE_SHP);
$data['geometry']['parts'][$i] = [
'numpoints' => 0,
'points' => [],
];
}
// Points
$part = 0;
for ($i = 0; $i < $tot_points; ++$i) {
if (isset($parts_first_index[$part + 1]) && $parts_first_index[$part + 1] == $i) {
++$part;
}
$data['geometry']['parts'][$part]['points'][] = $this->readXY();
}
for ($i = 0; $i < $data['geometry']['numparts']; ++$i) {
$data['geometry']['parts'][$i]['numpoints'] = count($data['geometry']['parts'][$i]['points']);
}
return $flag_return_geometry ? $this->createLinestring($data) : $data;
}
/**
* Reads a PolyLineM from the SHP file.
*
* @param bool $flag_return_geometry Flag to control return type.
*
* @return \Shapefile\Geometry\Linestring|\Shapefile\Geometry\MultiLinestring|array
*/
private function readPolyLineM($flag_return_geometry = true)
{
// PolyLine
$data = $this->readPolyLine(false);
// M Range
$data['bbox'] += $this->readMRange();
// M Array
for ($i = 0; $i < $data['geometry']['numparts']; ++$i) {
for ($k = 0; $k < $data['geometry']['parts'][$i]['numpoints']; ++$k) {
$data['geometry']['parts'][$i]['points'][$k] += $this->readM();
}
}
return $flag_return_geometry ? $this->createLinestring($data) : $data;
}
/**
* Reads a PolyLineZ from the SHP file.
*
* @param bool $flag_return_geometry Flag to control return type.
*
* @return \Shapefile\Geometry\Linestring|\Shapefile\Geometry\MultiLinestring|array
*/
private function readPolyLineZ($flag_return_geometry = true)
{
// PolyLine
$data = $this->readPolyLine(false);
// Z Range
$data['bbox'] += $this->readZRange();
// Z Array
for ($i = 0; $i < $data['geometry']['numparts']; ++$i) {
for ($k = 0; $k < $data['geometry']['parts'][$i]['numpoints']; ++$k) {
$data['geometry']['parts'][$i]['points'][$k] += $this->readZ();
}
}
// M Range
$data['bbox'] += $this->readMRange();
// M Array
for ($i = 0; $i < $data['geometry']['numparts']; ++$i) {
for ($k = 0; $k < $data['geometry']['parts'][$i]['numpoints']; ++$k) {
$data['geometry']['parts'][$i]['points'][$k] += $this->readM();
}
}
return $flag_return_geometry ? $this->createLinestring($data) : $data;
}
/**
* Helper method to create the actual Linestring Geometry using data read from SHP file.
* If OPTION_FORCE_MULTIPART_GEOMETRIES is set, a MultiLinestring is returned instead.
*
* @param array $data Array with "bbox" and "geometry" values.
*
* @return \Shapefile\Geometry\Linestring|\Shapefile\Geometry\MultiLinestring
*/
private function createLinestring($data)
{
if (!$this->getOption(Shapefile::OPTION_FORCE_MULTIPART_GEOMETRIES) && $data['geometry']['numparts'] == 1) {
$data['geometry'] = $data['geometry']['parts'][0];
$Geometry = new Linestring();
} else {
$Geometry = new MultiLinestring();
}
$Geometry->initFromArray($data['geometry']);
if (!$this->getOption(Shapefile::OPTION_IGNORE_GEOMETRIES_BBOXES)) {
$Geometry->setCustomBoundingBox($data['bbox']);
}
return $Geometry;
}
/**
* Reads a Polygon from the SHP file.
*
* @return \Shapefile\Geometry\Polygon|\Shapefile\Geometry\MultiPolygon
*/
private function readPolygon()
{
return $this->createPolygon($this->readPolyLine(false));
}
/**
* Reads a PolygonM from the SHP file.
*
* @return \Shapefile\Geometry\Polygon|\Shapefile\Geometry\MultiPolygon
*/
private function readPolygonM()
{
return $this->createPolygon($this->readPolyLineM(false));
}
/**
* Reads a PolygonZ from the SHP file.
*
* @return \Shapefile\Geometry\Polygon|\Shapefile\Geometry\MultiPolygon
*/
private function readPolygonZ()
{
return $this->createPolygon($this->readPolyLineZ(false));
}
/**
* Helper method to create the actual Polygon Geometry using data read from SHP file.
* If OPTION_FORCE_MULTIPART_GEOMETRIES is set, a MultiPolygon is returned instead.
*
* @param array $data Array with "bbox" and "geometry" values.
*
* @return \Shapefile\Geometry\Polygon|\Shapefile\Geometry\MultiPolygon
*/
private function createPolygon($data)
{
$MultiPolygon = new MultiPolygon(null, $this->getOption(Shapefile::OPTION_POLYGON_CLOSED_RINGS_ACTION), $this->getOption(Shapefile::OPTION_POLYGON_OUTPUT_ORIENTATION));
$Polygon = null;
$temp_state = null;
foreach ($data['geometry']['parts'] as $part) {
$Linestring = new Linestring();
$Linestring->initFromArray($part);
$is_clockwise = $Linestring->isClockwise();
if ($Polygon === null && !$is_clockwise && !$this->getOption(Shapefile::OPTION_POLYGON_ORIENTATION_READING_AUTOSENSE)) {
throw new ShapefileException(Shapefile::ERR_GEOM_POLYGON_WRONG_ORIENTATION);
}
if ($temp_state === null || $temp_state === $is_clockwise) {
if ($Polygon !== null) {
$MultiPolygon->addPolygon($Polygon);
}
$Polygon = new Polygon(null, $this->getOption(Shapefile::OPTION_POLYGON_CLOSED_RINGS_ACTION), $this->getOption(Shapefile::OPTION_POLYGON_OUTPUT_ORIENTATION));
$temp_state = $is_clockwise;
}
$Polygon->addRing($Linestring);
}
$MultiPolygon->addPolygon($Polygon);
$Geometry = (!$this->getOption(Shapefile::OPTION_FORCE_MULTIPART_GEOMETRIES) && $MultiPolygon->getNumPolygons() == 1) ? $MultiPolygon->getPolygon(0) : $MultiPolygon;
if (!$this->getOption(Shapefile::OPTION_IGNORE_GEOMETRIES_BBOXES)) {
$Geometry->setCustomBoundingBox($data['bbox']);
}
return $Geometry;
}
}