melder/vendor/Shapefile/ShapefileReader.php
2024-02-16 15:35:01 +01:00

958 lines
33 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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;
}
}