<?php /** * PHP Shapefile - PHP library to read and write ESRI Shapefiles, compatible with WKT and GeoJSON * * @package Shapefile * @author Gaspare Sganga * @version 4.0.0dev * @license MIT * @link https://gasparesganga.com/labs/php-shapefile/ */ namespace Shapefile; use Shapefile\File\FileInterface; use Shapefile\File\StreamResourceFile; /** * Abstract base class for ShapefileReader and ShapefileWriter. * It provides some common public methods to both of them and exposes package-wide constants. * * Efforts have been made all throughout the library to keep it compatible with an audience * as broader as possible and some "stylistic tradeoffs" here and there were necessary to support PHP 5.4. */ abstract class Shapefile { /////////////////////////////// PUBLIC CONSTANTS /////////////////////////////// /** Actions */ const ACTION_IGNORE = 0; const ACTION_CHECK = 1; const ACTION_FORCE = 2; /** DBF fields types */ const DBF_TYPE_CHAR = 'C'; const DBF_TYPE_DATE = 'D'; const DBF_TYPE_LOGICAL = 'L'; const DBF_TYPE_MEMO = 'M'; const DBF_TYPE_NUMERIC = 'N'; const DBF_TYPE_FLOAT = 'F'; /** File types */ const FILE_SHP = 'shp'; const FILE_SHX = 'shx'; const FILE_DBF = 'dbf'; const FILE_DBT = 'dbt'; const FILE_PRJ = 'prj'; const FILE_CPG = 'cpg'; /** Return formats */ const FORMAT_INT = 0; const FORMAT_STR = 1; /** File modes */ const MODE_PRESERVE = 0; const MODE_OVERWRITE = 1; const MODE_APPEND = 2; /** Polygon orientations */ const ORIENTATION_CLOCKWISE = 0; const ORIENTATION_COUNTERCLOCKWISE = 1; const ORIENTATION_UNCHANGED = 2; /** Shape types */ const SHAPE_TYPE_NULL = 0; const SHAPE_TYPE_POINT = 1; const SHAPE_TYPE_POLYLINE = 3; const SHAPE_TYPE_POLYGON = 5; const SHAPE_TYPE_MULTIPOINT = 8; const SHAPE_TYPE_POINTZ = 11; const SHAPE_TYPE_POLYLINEZ = 13; const SHAPE_TYPE_POLYGONZ = 15; const SHAPE_TYPE_MULTIPOINTZ = 18; const SHAPE_TYPE_POINTM = 21; const SHAPE_TYPE_POLYLINEM = 23; const SHAPE_TYPE_POLYGONM = 25; const SHAPE_TYPE_MULTIPOINTM = 28; /** Misc */ const EOF = 0; const UNDEFINED = null; const UNKNOWN = -1; /////////////////////////////// OPTIONS /////////////////////////////// /** * Number of records to keep into buffer before writing them. * Use a value equal or less than 0 to keep all records into buffer and write them at once. * ShapefileWriter * @var int */ const OPTION_BUFFERED_RECORDS = 'OPTION_BUFFERED_RECORDS'; const OPTION_BUFFERED_RECORDS_DEFAULT = 10; /** * Converts from input charset to UTF-8 all strings read from DBF files. * ShapefileWriter * @var bool */ const OPTION_CPG_ENABLE_FOR_DEFAULT_CHARSET = 'OPTION_CPG_ENABLE_FOR_DEFAULT_CHARSET'; const OPTION_CPG_ENABLE_FOR_DEFAULT_CHARSET_DEFAULT = false; /** * Allows a maximum field size of 255 bytes instead of 254 bytes in DBF files. * ShapefileReader and ShapefileWriter * @var bool */ const OPTION_DBF_ALLOW_FIELD_SIZE_255 = 'OPTION_DBF_ALLOW_FIELD_SIZE_255'; const OPTION_DBF_ALLOW_FIELD_SIZE_255_DEFAULT = false; /** * Converts from input charset to UTF-8 all strings read from DBF files. * ShapefileReader * @var bool */ const OPTION_DBF_CONVERT_TO_UTF8 = 'OPTION_DBF_CONVERT_TO_UTF8'; const OPTION_DBF_CONVERT_TO_UTF8_DEFAULT = true; /** * Forces all capitals field names in DBF files. * ShapefileReader and ShapefileWriter * @var bool */ const OPTION_DBF_FORCE_ALL_CAPS = 'OPTION_DBF_FORCE_ALL_CAPS'; const OPTION_DBF_FORCE_ALL_CAPS_DEFAULT = true; /** * Ignored fields in DBF file. * An array of fields to ignore when reading the DBF file. * ShapefileReader * @var array|null */ const OPTION_DBF_IGNORED_FIELDS = 'OPTION_DBF_IGNORED_FIELDS'; const OPTION_DBF_IGNORED_FIELDS_DEFAULT = null; /** * Defines a null padding character to represent null values in DBF files. * ShapefileReader and ShapefileWriter * @var string|null */ const OPTION_DBF_NULL_PADDING_CHAR = 'OPTION_DBF_NULL_PADDING_CHAR'; const OPTION_DBF_NULL_PADDING_CHAR_DEFAULT = null; /** * Returns a null value for invalid dates when reading DBF files and nullify invalid dates when writing them. * ShapefileReader and ShapefileWriter * @var bool */ const OPTION_DBF_NULLIFY_INVALID_DATES = 'OPTION_DBF_NULLIFY_INVALID_DATES'; const OPTION_DBF_NULLIFY_INVALID_DATES_DEFAULT = true; /** * Returns dates as DateTime objects instead of ISO strings (YYYY-MM-DD). * ShapefileReader * @var bool */ const OPTION_DBF_RETURN_DATES_AS_OBJECTS = 'OPTION_DBF_RETURN_DATES_AS_OBJECTS'; const OPTION_DBF_RETURN_DATES_AS_OBJECTS_DEFAULT = false; /** * Deletes empty files after closing them. * This makes sense only when they were passed as resource handles or FileInterface instances. * ShapefileWriter * @var bool */ const OPTION_DELETE_EMPTY_FILES = 'OPTION_DELETE_EMPTY_FILES'; const OPTION_DELETE_EMPTY_FILES_DEFAULT = true; /** * Enforces Geometries to have all data fields defined in Shapefile. * ShapefileWriter * @var bool */ const OPTION_ENFORCE_GEOMETRY_DATA_STRUCTURE = 'OPTION_ENFORCE_GEOMETRY_DATA_STRUCTURE'; const OPTION_ENFORCE_GEOMETRY_DATA_STRUCTURE_DEFAULT = true; /** * Defines behaviour with existing files with the same name. * Possible values: * MODE_PRESERVE : Throws Shapefile::ERR_FILE_EXISTS * MODE_OVERWRITE : Overwrites existing files * MODE_APPEND : Appends new records to existing files * ShapefileWriter * @var int */ const OPTION_EXISTING_FILES_MODE = 'OPTION_EXISTING_FILES_MODE'; const OPTION_EXISTING_FILES_MODE_DEFAULT = self::MODE_PRESERVE; /** * Reads all Polyline and Polygon Geometries as Multi. * ShapefileReader * @var bool */ const OPTION_FORCE_MULTIPART_GEOMETRIES = 'OPTION_FORCE_MULTIPART_GEOMETRIES'; const OPTION_FORCE_MULTIPART_GEOMETRIES_DEFAULT = false; /** * Ignores DBF file (useful to recover corrupted Shapefiles). * Data will not be available for geometries. * ShapefileReader * @var bool */ const OPTION_IGNORE_FILE_DBF = 'OPTION_IGNORE_FILE_DBF'; const OPTION_IGNORE_FILE_DBF_DEFAULT = false; /** * Ignores SHX file (useful to recover corrupted Shapefiles). * This might not always work as it relies on SHP record headers content lengths * and assumes there are no unused bytes between records in SHP file. * Random access to specific records will not be possible. * ShapefileReader * @var bool */ const OPTION_IGNORE_FILE_SHX = 'OPTION_IGNORE_FILE_SHX'; const OPTION_IGNORE_FILE_SHX_DEFAULT = false; /** * Ignores Geometries bounding box found in Shapefile. * ShapefileReader * @var bool */ const OPTION_IGNORE_GEOMETRIES_BBOXES = 'OPTION_IGNORE_GEOMETRIES_BBOXES'; const OPTION_IGNORE_GEOMETRIES_BBOXES_DEFAULT = false; /** * Ignores bounding box found in Shapefile. * ShapefileReader * @var bool */ const OPTION_IGNORE_SHAPEFILE_BBOX = 'OPTION_IGNORE_SHAPEFILE_BBOX'; const OPTION_IGNORE_SHAPEFILE_BBOX_DEFAULT = false; /** * Defines action to perform on Polygons rings. * They should be closed but some software do not enforce that, creating uncompliant Shapefiles. * Possible values: * Shapefile::ACTION_IGNORE : No action taken * Shapefile::ACTION_CHECK : Checks for open rings and eventually throws Shapefile::ERR_GEOM_POLYGON_OPEN_RING * Shapefile::ACTION_FORCE : Forces all rings to be closed in Polygons * ShapefileReader * @var int */ const OPTION_POLYGON_CLOSED_RINGS_ACTION = 'OPTION_POLYGON_CLOSED_RINGS_ACTION'; const OPTION_POLYGON_CLOSED_RINGS_ACTION_DEFAULT = self::ACTION_CHECK; /** * Allows Polygons orientation to be either clockwise or counterclockwise when reading Shapefiles. * Set to false to enforce strict ESRI Shapefile specs (clockwise outer rings and counterclockwise inner ones) * and raise a Shapefile::ERR_GEOM_POLYGON_WRONG_ORIENTATION error for uncompliant Shapefiles. * ShapefileReader * @var bool */ const OPTION_POLYGON_ORIENTATION_READING_AUTOSENSE = 'OPTION_POLYGON_ORIENTATION_READING_AUTOSENSE'; const OPTION_POLYGON_ORIENTATION_READING_AUTOSENSE_DEFAULT = true; /** * Forces a specific orientation for Polygons after reading them. * ESRI Shapefile specs establish clockwise orientation for outer rings and counterclockwise for inner ones, * GeoJSON require the opposite (counterclockwise outer rings and clockwise inner ones) * and Simple Features used to be the same as GeoJSON but is currently allowing both. * Possible values: * Shapefile::ORIENTATION_CLOCKWISE : Forces clockwise outer ring and counterclockwise inner rings * Shapefile::ORIENTATION_COUNTERCLOCKWISE : Forces counterclockwise outer ring and clockwise inner rings * Shapefile::ORIENTATION_UNCHANGED : Preserves original Shapefile orientation depending on file * ShapefileReader * @var int */ const OPTION_POLYGON_OUTPUT_ORIENTATION = 'OPTION_POLYGON_OUTPUT_ORIENTATION'; const OPTION_POLYGON_OUTPUT_ORIENTATION_DEFAULT = self::ORIENTATION_COUNTERCLOCKWISE; /** * Suppresses M dimension. * ShapefileReader and ShapefileWriter * @var bool */ const OPTION_SUPPRESS_M = 'OPTION_SUPPRESS_M'; const OPTION_SUPPRESS_M_DEFAULT = false; /** * Suppresses Z dimension. * ShapefileReader and ShapefileWriter * @var bool */ const OPTION_SUPPRESS_Z = 'OPTION_SUPPRESS_Z'; const OPTION_SUPPRESS_Z_DEFAULT = false; /////////////////////////////// ERRORS /////////////////////////////// const ERR_UNDEFINED = 'ERR_UNDEFINED'; const ERR_UNDEFINED_MESSAGE = "Undefined error."; const ERR_FILE_MISSING = 'ERR_FILE_MISSING'; const ERR_FILE_MISSING_MESSAGE = "A required file is missing"; const ERR_FILE_EXISTS = 'ERR_FILE_EXISTS'; const ERR_FILE_EXISTS_MESSAGE = "Check if the file exists and is readable and/or writable"; const ERR_FILE_PERMISSIONS = 'ERR_FILE_PERMISSIONS'; const ERR_FILE_PERMISSIONS_MESSAGE = "Check if the file is readable and/or writable in binary mode"; const ERR_FILE_PATH_NOT_VALID = 'ERR_FILE_PATH_NOT_VALID'; const ERR_FILE_PATH_NOT_VALID_MESSAGE = "File path not valid"; const ERR_FILE_RESOURCE_NOT_VALID = 'ERR_FILE_RESOURCE_NOT_VALID'; const ERR_FILE_RESOURCE_NOT_VALID_MESSAGE = "File pointer resource not valid. It must be a seekable stream"; const ERR_FILE_OPEN = 'ERR_FILE_OPEN'; const ERR_FILE_OPEN_MESSAGE = "Unable to open file"; const ERR_FILE_READING = 'ERR_FILE_READING'; const ERR_FILE_READING_MESSAGE = "Error during binary file reading"; const ERR_FILE_WRITING = 'ERR_FILE_WRITING'; const ERR_FILE_WRITING_MESSAGE = "Error during binary file writing"; const ERR_SHP_TYPE_NOT_SUPPORTED = 'ERR_SHP_TYPE_NOT_SUPPORTED'; const ERR_SHP_TYPE_NOT_SUPPORTED_MESSAGE = "Shape type not supported"; const ERR_SHP_TYPE_NOT_SET = 'ERR_SHP_TYPE_NOT_SET'; const ERR_SHP_TYPE_NOT_SET_MESSAGE = "Shape type not set"; const ERR_SHP_TYPE_ALREADY_SET = 'ERR_SHP_TYPE_ALREADY_SET'; const ERR_SHP_TYPE_ALREADY_SET_MESSAGE = "Shape type has already been set"; const ERR_SHP_GEOMETRY_TYPE_NOT_COMPATIBLE = 'ERR_SHP_GEOMETRY_TYPE_NOT_COMPATIBLE'; const ERR_SHP_GEOMETRY_TYPE_NOT_COMPATIBLE_MESSAGE = "Geometry type must be compatible with Shapefile shape type"; const ERR_SHP_MISMATCHED_BBOX = 'ERR_SHP_MISMATCHED_BBOX'; const ERR_SHP_MISMATCHED_BBOX_MESSAGE = "Bounding box must have the same dimensions as the Shapefile (2D, 3D or 4D)"; const ERR_SHP_FILE_ALREADY_INITIALIZED = 'ERR_SHP_FILE_ALREADY_INITIALIZED'; const ERR_SHP_FILE_ALREADY_INITIALIZED_MESSAGE = "Cannot change Shapefile definition after it has been initialized with data"; const ERR_SHP_WRONG_RECORD_TYPE = 'ERR_SHP_WRONG_RECORD_TYPE'; const ERR_SHP_WRONG_RECORD_TYPE_MESSAGE = "Wrong record shape type"; const ERR_DBF_FILE_NOT_VALID = 'ERR_DBF_FILE_NOT_VALID'; const ERR_DBF_FILE_NOT_VALID_MESSAGE = "DBF file doesn't seem to be a valid dBase III or dBase IV format"; const ERR_DBF_MISMATCHED_FILE = 'ERR_DBF_MISMATCHED_FILE'; const ERR_DBF_MISMATCHED_FILE_MESSAGE = "Mismatched DBF file. Number of records not corresponding to the SHP file"; const ERR_DBF_EOF_REACHED = 'ERR_DBF_EOF_REACHED'; const ERR_DBF_EOF_REACHED_MESSAGE = "End of DBF file reached. Number of records not corresponding to the SHP file"; const ERR_DBF_MAX_FIELD_COUNT_REACHED = 'ERR_DBF_MAX_FIELD_COUNT_REACHED'; const ERR_DBF_MAX_FIELD_COUNT_REACHED_MESSAGE = "Cannot add other fields, maximum number of fields in a DBF file reached"; const ERR_DBF_FIELD_NAME_NOT_VALID = 'ERR_DBF_FIELD_NAME_NOT_VALID'; const ERR_DBF_FIELD_NAME_NOT_VALID_MESSAGE = "Too many field names conflicting"; const ERR_DBF_FIELD_TYPE_NOT_VALID = 'ERR_DBF_FIELD_TYPE_NOT_VALID'; const ERR_DBF_FIELD_TYPE_NOT_VALID_MESSAGE = "Field type must be CHAR, DATE, LOGICAL, MEMO or NUMERIC"; const ERR_DBF_FIELD_SIZE_NOT_VALID = 'ERR_DBF_FIELD_SIZE_NOT_VALID'; const ERR_DBF_FIELD_SIZE_NOT_VALID_MESSAGE = "Field size incorrect according to its type"; const ERR_DBF_FIELD_DECIMALS_NOT_VALID = 'ERR_DBF_FIELD_DECIMALS_NOT_VALID'; const ERR_DBF_FIELD_DECIMALS_NOT_VALID_MESSAGE = "Field decimals incorrect according to its type"; const ERR_DBF_CHARSET_CONVERSION = 'ERR_DBF_CHARSET_CONVERSION'; const ERR_DBF_CHARSET_CONVERSION_MESSAGE = "Error during conversion from provided DBF input charset to UTF-8"; const ERR_DBT_EOF_REACHED = 'ERR_DBT_EOF_REACHED'; const ERR_DBT_EOF_REACHED_MESSAGE = "End of DBT file reached. File might be corrupted"; const ERR_GEOM_NOT_EMPTY = 'ERR_GEOM_NOT_EMPTY'; const ERR_GEOM_NOT_EMPTY_MESSAGE = "Cannot reinitialize non-empty Geometry"; const ERR_GEOM_COORD_VALUE_NOT_VALID = 'ERR_GEOM_COORD_VALUE_NOT_VALID'; const ERR_GEOM_COORD_VALUE_NOT_VALID_MESSAGE = "Invalid coordinate value"; const ERR_GEOM_MISMATCHED_DIMENSIONS = 'ERR_GEOM_MISMATCHED_DIMENSIONS'; const ERR_GEOM_MISMATCHED_DIMENSIONS_MESSAGE = "All geometries in a collection must have the same dimensions (2D, 3D or 4D)"; const ERR_GEOM_MISMATCHED_BBOX = 'ERR_GEOM_MISMATCHED_BBOX'; const ERR_GEOM_MISMATCHED_BBOX_MESSAGE = "Bounding box must have the same dimensions as the Geometry (2D, 3D or 4D)"; const ERR_GEOM_MISSING_FIELD = 'ERR_GEOM_MISSING_FIELD'; const ERR_GEOM_MISSING_FIELD_MESSAGE = "Geometry is missing a field defined in the Shapefile"; const ERR_GEOM_POINT_NOT_VALID = 'ERR_GEOM_POINT_NOT_VALID'; const ERR_GEOM_POINT_NOT_VALID_MESSAGE = "A Point can be either EMPTY or al least 2D"; const ERR_GEOM_POLYGON_OPEN_RING = 'ERR_GEOM_POLYGON_OPEN_RING'; const ERR_GEOM_POLYGON_OPEN_RING_MESSAGE = "Polygons cannot contain open rings"; const ERR_GEOM_POLYGON_WRONG_ORIENTATION = 'ERR_GEOM_POLYGON_WRONG_ORIENTATION'; const ERR_GEOM_POLYGON_WRONG_ORIENTATION_MESSAGE = "Polygon orientation not compliant with Shapefile specifications"; const ERR_GEOM_RING_AREA_TOO_SMALL = 'ERR_GEOM_RING_AREA_TOO_SMALL'; const ERR_GEOM_RING_AREA_TOO_SMALL_MESSAGE = "Ring area too small. Cannot determine ring orientation"; const ERR_GEOM_RING_NOT_ENOUGH_VERTICES = 'ERR_GEOM_RING_NOT_ENOUGH_VERTICES'; const ERR_GEOM_RING_NOT_ENOUGH_VERTICES_MESSAGE = "Not enough vertices. Cannot determine ring orientation"; const ERR_INPUT_RANDOM_ACCESS_UNAVAILABLE = 'ERR_INPUT_RANDOM_ACCESS_UNAVAILABLE'; const ERR_INPUT_RANDOM_ACCESS_UNAVAILABLE_MESSAGE = "Cannot change current record without a valid SHX file"; const ERR_INPUT_RECORD_NOT_FOUND = 'ERR_INPUT_RECORD_NOT_FOUND'; const ERR_INPUT_RECORD_NOT_FOUND_MESSAGE = "Record index not found (check the total number of records in the SHP file)"; const ERR_INPUT_FIELD_NOT_FOUND = 'ERR_INPUT_FIELD_NOT_FOUND'; const ERR_INPUT_FIELD_NOT_FOUND_MESSAGE = "Field not found"; const ERR_INPUT_GEOMETRY_TYPE_NOT_VALID = 'ERR_INPUT_GEOMETRY_TYPE_NOT_VALID'; const ERR_INPUT_GEOMETRY_TYPE_NOT_VALID_MESSAGE = "Geometry type not valid. Must be of specified type"; const ERR_INPUT_GEOMETRY_INDEX_NOT_VALID = 'ERR_INPUT_GEOMETRY_INDEX_NOT_VALID'; const ERR_INPUT_GEOMETRY_INDEX_NOT_VALID_MESSAGE = "Geometry index not valid (check the total number of geometries in the collection)"; const ERR_INPUT_ARRAY_NOT_VALID = 'ERR_INPUT_ARRAY_NOT_VALID'; const ERR_INPUT_ARRAY_NOT_VALID_MESSAGE = "Array not valid"; const ERR_INPUT_WKT_NOT_VALID = 'ERR_INPUT_WKT_NOT_VALID'; const ERR_INPUT_WKT_NOT_VALID_MESSAGE = "WKT not valid"; const ERR_INPUT_GEOJSON_NOT_VALID = 'ERR_INPUT_GEOJSON_NOT_VALID'; const ERR_INPUT_GEOJSON_NOT_VALID_MESSAGE = "GeoJSON not valid"; const ERR_INPUT_NUMERIC_VALUE_OVERFLOW = 'ERR_INPUT_NUMERIC_VALUE_OVERFLOW'; const ERR_INPUT_NUMERIC_VALUE_OVERFLOW_MESSAGE = "Integer value overflows field size definition"; /////////////////////////////// DEPRECATED CONSTANTS /////////////////////////////// /** * @deprecated This option was deprecated with v3.3.0 and will disappear in the next releases. * Use OPTION_POLYGON_CLOSED_RINGS_ACTION instead. */ const OPTION_ENFORCE_POLYGON_CLOSED_RINGS = 'OPTION_ENFORCE_POLYGON_CLOSED_RINGS'; /** * @deprecated This option was deprecated with v3.3.0 and will disappear in the next releases. * Use OPTION_POLYGON_OUTPUT_ORIENTATION instead. */ const OPTION_INVERT_POLYGONS_ORIENTATION = 'OPTION_INVERT_POLYGONS_ORIENTATION'; /** * @deprecated This constant was deprecated with v3.5.0 and will disappear in the next releases. * Use ERR_FILE_RESOURCE_NOT_VALID instead. */ const ERR_FILE_INVALID_RESOURCE = 'ERR_FILE_RESOURCE_NOT_VALID'; /** * @deprecated This constant was deprecated with v3.3.0 and will disappear in the next releases. * Use ERR_GEOM_RING_AREA_TOO_SMALL instead. */ const ERR_GEOM_POLYGON_AREA_TOO_SMALL = 'ERR_GEOM_RING_AREA_TOO_SMALL'; /** * @deprecated This constant was deprecated with v3.3.0 and will disappear in the next releases. * Use ERR_GEOM_POLYGON_WRONG_ORIENTATION instead. */ const ERR_GEOM_POLYGON_NOT_VALID = 'ERR_GEOM_POLYGON_WRONG_ORIENTATION'; /////////////////////////////// INTERNAL CONSTANTS /////////////////////////////// /** SHP files constants */ const SHP_FILE_CODE = 9994; const SHP_HEADER_SIZE = 100; const SHP_NO_DATA_THRESHOLD = -1e38; const SHP_NO_DATA_VALUE = -1e40; const SHP_REC_HEADER_SIZE = 8; const SHP_VERSION = 1000; /** SHX files constants */ const SHX_HEADER_SIZE = 100; const SHX_RECORD_SIZE = 8; /** DBF files constants */ const DBF_BLANK = 0x20; const DBF_DEFAULT_CHARSET = 'ISO-8859-1'; const DBF_DELETED_MARKER = 0x2a; const DBF_EOF_MARKER = 0x1a; const DBF_FIELD_TERMINATOR = 0x0d; const DBF_MAX_FIELD_COUNT = 255; const DBF_VALUE_MASK_FALSE = 'FfNn0'; const DBF_VALUE_MASK_TRUE = 'TtYy1'; const DBF_VALUE_FALSE = 'F'; const DBF_VALUE_NULL = '?'; const DBF_VALUE_TRUE = 'T'; const DBF_VERSION = 0x03; const DBF_VERSION_WITH_DBT = 0x83; /** DBT files constants */ const DBT_BLOCK_SIZE = 512; const DBT_FIELD_TERMINATOR = 0x1a; /** Shape types text description */ public static $shape_types = [ self::SHAPE_TYPE_NULL => 'Null Shape', self::SHAPE_TYPE_POINT => 'Point', self::SHAPE_TYPE_POLYLINE => 'PolyLine', self::SHAPE_TYPE_POLYGON => 'Polygon', self::SHAPE_TYPE_MULTIPOINT => 'MultiPoint', self::SHAPE_TYPE_POINTZ => 'PointZ', self::SHAPE_TYPE_POLYLINEZ => 'PolyLineZ', self::SHAPE_TYPE_POLYGONZ => 'PolygonZ', self::SHAPE_TYPE_MULTIPOINTZ => 'MultiPointZ', self::SHAPE_TYPE_POINTM => 'PointM', self::SHAPE_TYPE_POLYLINEM => 'PolyLineM', self::SHAPE_TYPE_POLYGONM => 'PolygonM', self::SHAPE_TYPE_MULTIPOINTM => 'MultiPointM', ]; /////////////////////////////// PRIVATE VARIABLES /////////////////////////////// /** * @var int|null Shapefile type. */ private $shape_type = null; /** * @var array|null Custom bounding box set with setCustomBoundingBox() method. */ private $custom_bounding_box = null; /** * @var array|null Computed bounding box. */ private $computed_bounding_box = null; /** * @var string|null PRJ well-known-text. */ private $prj = null; /** * @var string|null DBF charset. */ private $charset = null; /** * @var array Fields definition. * Every field is represented by an array with the following structure: * [ * "type" => string * "size" => int * "decimals" => int * ] */ private $fields = []; /** * @var array Array of FileInterface instances. */ private $files = []; /** * @var array Array of canonicalized absolute pathnames of open files. * It will be populated only if files are NOT passed as stream resources. */ private $filenames = []; /** * @var array Options. */ private $options = []; /** * @var int Total number of records. */ private $tot_records; /** * @var bool|null Flag to store whether the machine is big endian or not. */ private $flag_big_endian_machine = null; /** * @var bool Flag representing whether the Shapefile has been initialized with any Geometry or not. */ private $flag_initialized = false; /////////////////////////////// PUBLIC /////////////////////////////// /** * Checks if Shapefile is of type Z. * * @return bool */ public function isZ() { $shape_type = $this->getShapeType(Shapefile::FORMAT_INT); return $shape_type > 10 && $shape_type < 20; } /** * Checks if Shapefile is of type M. * * @return bool */ public function isM() { return $this->getShapeType(Shapefile::FORMAT_INT) > 10; } /** * Gets shape type either as integer or string. * * @param int $format Optional desired output format. * It can be on of the following: * - Shapefile::FORMAT_INT [default] * - Shapefile::FORMAT_STR * * @return int|string */ public function getShapeType($format = Shapefile::FORMAT_INT) { if ($this->shape_type === null) { throw new ShapefileException(Shapefile::ERR_SHP_TYPE_NOT_SET); } if ($format == Shapefile::FORMAT_STR) { return Shapefile::$shape_types[$this->shape_type]; } else { return $this->shape_type; } } /** * Gets Shapefile bounding box. * * @return array */ public function getBoundingBox() { return $this->custom_bounding_box ?: $this->computed_bounding_box; } /** * Gets PRJ well-known-text. * * @return string */ public function getPRJ() { return $this->prj; } /** * Gets DBF charset. * * @return string */ public function getCharset() { return $this->charset ?: Shapefile::DBF_DEFAULT_CHARSET; } /** * Sets or resets DBF charset. * * @param mixed $charset Name of the charset. * Pass a falsy value (eg. false or "") to reset it to default. * * @return self Returns $this to provide a fluent interface. */ public function setCharset($charset) { $this->charset = $charset ?: Shapefile::DBF_DEFAULT_CHARSET; return $this; } /** * Gets all fields names. * * @return array */ public function getFieldsNames() { return array_keys($this->fields); } /** * Gets all fields definitions. * * @return array */ public function getFields() { return $this->fields; } /** * Gets a field type. * * @param string $name Name of the field. * * @return string */ public function getFieldType($name) { return $this->getField($name)['type']; } /** * Gets a field size. * * @param string $name Name of the field. * * @return int */ public function getFieldSize($name) { return $this->getField($name)['size']; } /** * Gets a field decimals. * * @param string $name Name of the field. * * @return int */ public function getFieldDecimals($name) { return $this->getField($name)['decimals']; } /** * Gets a complete field definition. * * The returned array contains the following elements: * [ * "type" => string * "size" => int * "decimals" => int * ] * * @param string $name Name of the field. * * @return array */ public function getField($name) { $name = $this->normalizeDBFFieldNameCase($name); if (!isset($this->fields[$name])) { throw new ShapefileException(Shapefile::ERR_INPUT_FIELD_NOT_FOUND, $name); } return $this->fields[$name]; } /** * Gets total number of records in SHP and DBF files. * * @return int */ public function getTotRecords() { return $this->tot_records; } /////////////////////////////// PROTECTED /////////////////////////////// /** * Opens files with binary read or write access. * * Filenames are mapped here because files are closed in destructors and working directory might be different! * * @param string|array $files Path to SHP file / Array of paths / Array of resource handles / Array of FileInterface instances. * @param bool $write_access Access type: false = read; true = write. * @param array $ignored_files Optional map of files to ignore [filetype => bool]. * * @return self Returns $this to provide a fluent interface. */ protected function openFiles($files, $write_access, $ignored_files = []) { // Create $files array from single string (SHP filename) if (is_string($files)) { $basename = (substr($files, -4) == '.' . Shapefile::FILE_SHP) ? substr($files, 0, -4) : $files; $files = [ Shapefile::FILE_SHP => $basename . '.' . Shapefile::FILE_SHP, Shapefile::FILE_SHX => $basename . '.' . Shapefile::FILE_SHX, Shapefile::FILE_DBF => $basename . '.' . Shapefile::FILE_DBF, Shapefile::FILE_DBT => $basename . '.' . Shapefile::FILE_DBT, Shapefile::FILE_PRJ => $basename . '.' . Shapefile::FILE_PRJ, Shapefile::FILE_CPG => $basename . '.' . Shapefile::FILE_CPG, ]; } // Ignored files $ignored_files = $ignored_files + [ Shapefile::FILE_SHX => false, Shapefile::FILE_DBF => false, ]; // Make sure required files are specified foreach ( [ Shapefile::FILE_SHP, Shapefile::FILE_SHX, Shapefile::FILE_DBF, ] as $type ) { if (!is_array($files) || (!isset($files[$type]) && (!isset($ignored_files[$type]) || !$ignored_files[$type]))) { throw new ShapefileException(Shapefile::ERR_FILE_MISSING, strtoupper($type)); } } $this->filenames = []; if (array_reduce($files, function($ret, $item) { return $ret && $item instanceof FileInterface; }, true)) { // FileInterface instances foreach ($files as $type => $File) { if (!isset($ignored_files[$type]) || !$ignored_files[$type]) { if ((!$write_access && !$File->isReadable()) || ($write_access && !$File->isWritable())) { throw new ShapefileException(Shapefile::ERR_FILE_PERMISSIONS, strtoupper($type)); } $this->files[$type] = $File; } } } elseif ($files === array_filter($files, 'is_resource')) { // Resource handles foreach ($files as $type => $file) { if (!isset($ignored_files[$type]) || !$ignored_files[$type]) { try { $this->files[$type] = new StreamResourceFile($file, $write_access); } catch (ShapefileException $e) { throw new ShapefileException($e->getErrorType(), strtoupper($type)); } } } } else { // Filenames foreach ( [ Shapefile::FILE_SHP => true, Shapefile::FILE_SHX => !$ignored_files[Shapefile::FILE_SHX], Shapefile::FILE_DBF => !$ignored_files[Shapefile::FILE_DBF], Shapefile::FILE_DBT => false, Shapefile::FILE_PRJ => false, Shapefile::FILE_CPG => false, ] as $type => $required ) { if (isset($files[$type]) && (!isset($ignored_files[$type]) || !$ignored_files[$type])) { if (!is_string($files[$type])){ throw new ShapefileException(Shapefile::ERR_FILE_PATH_NOT_VALID, strtoupper($type)); } if ( (!$write_access && is_readable($files[$type]) && is_file($files[$type])) || ($write_access && is_writable(dirname($files[$type])) && (!file_exists($files[$type]) || (is_file($files[$type]) && is_writable($files[$type]) && (($this->getOption(Shapefile::OPTION_EXISTING_FILES_MODE) === Shapefile::MODE_APPEND && is_readable($files[$type])) || ($this->getOption(Shapefile::OPTION_EXISTING_FILES_MODE) === Shapefile::MODE_OVERWRITE))))) ) { try { $this->files[$type] = new StreamResourceFile($files[$type], $write_access); $this->filenames[$type] = $this->files[$type]->getFilepath(); } catch (ShapefileException $e) { throw new ShapefileException($e->getErrorType(), $files[$type]); } } elseif ($required) { throw new ShapefileException(Shapefile::ERR_FILE_EXISTS, $files[$type]); } } } } // Set files pointers to start foreach ($this->files as $File) { $File->setPointer(0); } return $this; } /** * Closes all open files. * Actually, this just destroys the reference to FileInterface instance, letting it handle the situation. * * @return self Returns $this to provide a fluent interface. */ protected function closeFiles() { foreach (array_keys($this->files) as $type) { $this->files[$type] = null; } return $this; } /** * Truncates file to given length. * * @param string $file_type File type. * @param int $size Optional size to truncate to. * * @return self Returns $this to provide a fluent interface. */ protected function fileTruncate($file_type, $size = 0) { $this->files[$file_type]->truncate($size); return $this; } /** * Checks if file type has been opened. * * @param string $file_type File type (member of $this->files array). * * @return bool */ protected function isFileOpen($file_type) { return isset($this->files[$file_type]); } /** * Gets an array of the open files. * * @return array */ protected function getFiles() { return $this->files; } /** * Gets an array of canonicalized absolute pathnames. * If files were passed as stream resource handles or FileInterface instances, an empty array is returned. * * @return array */ protected function getFilenames() { return $this->filenames; } /** * Gets file size. * * @param string $file_type File type (member of $this->files array). * * @return int */ protected function getFileSize($file_type) { return $this->files[$file_type]->getSize(); } /** * Gets file current pointer position. * * @param string $file_type File type (member of $this->files array). * * @return int */ protected function getFilePointer($file_type) { return $this->files[$file_type]->getPointer(); } /** * Sets file pointer to specified position. * * @param string $file_type File type (member of $this->files array). * @param int $position The position to set the pointer to. * * @return self Returns $this to provide a fluent interface. */ protected function setFilePointer($file_type, $position) { $this->files[$file_type]->setPointer($position); return $this; } /** * Resets file pointer position to its end. * * @param string $file_type File type (member of $this->files array). * * @return self Returns $this to provide a fluent interface. */ protected function resetFilePointer($file_type) { $this->files[$file_type]->resetPointer(); return $this; } /** * Increases file pointer position of specified offset. * * @param string $file_type File type (member of $this->files array). * @param int $offset The offset to move the pointer for. * * @return self Returns $this to provide a fluent interface. */ protected function setFileOffset($file_type, $offset) { $this->files[$file_type]->setOffset($offset); return $this; } /** * Reads data from file. * * @param string $file_type File type. * @param int $length Number of bytes to read. * * @return string */ protected function readData($file_type, $length) { $ret = $this->files[$file_type]->read($length); if ($ret === false) { throw new ShapefileException(Shapefile::ERR_FILE_READING); } return $ret; } /** * Writes binary string packed data to file. * * @param string $file_type File type. * @param string $data Binary string packed data to write. * * @return self Returns $this to provide a fluent interface. */ protected function writeData($file_type, $data) { if ($this->files[$file_type]->write($data) === false) { throw new ShapefileException(Shapefile::ERR_FILE_WRITING); } return $this; } /** * Checks if machine is big endian. * * @return bool */ protected function isBigEndianMachine() { if ($this->flag_big_endian_machine === null) { $this->flag_big_endian_machine = current(unpack('v', pack('S', 0xff))) !== 0xff; } return $this->flag_big_endian_machine; } /** * Initializes options with default and user-provided values. * * @param array $options_list Array of options to initialize. * @param array $user_values User-provided options values. * * @return self Returns $this to provide a fluent interface. */ protected function initOptions($options_list, $user_values) { // Make sure compulsory options used in this abstract class are defined $options_list = array_unique(array_merge($options_list, [ Shapefile::OPTION_DBF_ALLOW_FIELD_SIZE_255, Shapefile::OPTION_DBF_FORCE_ALL_CAPS, Shapefile::OPTION_SUPPRESS_M, Shapefile::OPTION_SUPPRESS_Z, ])); // Defaults $defaults = []; foreach ($options_list as $option) { $defaults[$option] = constant('Shapefile\Shapefile::' . $option . '_DEFAULT'); } // Filter custom options $user_values = array_intersect_key(array_change_key_case($user_values, CASE_UPPER), $defaults); // Initialize option array $this->options = $user_values + $defaults; // Use only the first character of OPTION_DBF_NULL_PADDING_CHAR if it's set and is not false or empty $k = Shapefile::OPTION_DBF_NULL_PADDING_CHAR; if (array_key_exists($k, $this->options)) { $this->options[$k] = ($this->options[$k] === false || $this->options[$k] === null || $this->options[$k] === '') ? null : substr($this->options[$k], 0, 1); } // Parse OPTION_DBF_IGNORED_FIELDS $k = Shapefile::OPTION_DBF_IGNORED_FIELDS; if (array_key_exists($k, $this->options)) { $this->options[$k] = is_array($this->options[$k]) ? array_map([$this, 'normalizeDBFFieldNameCase'], $this->options[$k]) : []; } return $this; } /** * Gets option value. * * @param string $option Name of the option. * * @return string */ protected function getOption($option) { return $this->options[$option]; } /** * Sets shape type. * It can be called just once for an instance of the class. * * @param int $type Shape type. It can be on of the following: * - Shapefile::SHAPE_TYPE_NULL * - Shapefile::SHAPE_TYPE_POINT * - Shapefile::SHAPE_TYPE_POLYLINE * - Shapefile::SHAPE_TYPE_POLYGON * - Shapefile::SHAPE_TYPE_MULTIPOINT * - Shapefile::SHAPE_TYPE_POINTZ * - Shapefile::SHAPE_TYPE_POLYLINEZ * - Shapefile::SHAPE_TYPE_POLYGONZ * - Shapefile::SHAPE_TYPE_MULTIPOINTZ * - Shapefile::SHAPE_TYPE_POINTM * - Shapefile::SHAPE_TYPE_POLYLINEM * - Shapefile::SHAPE_TYPE_POLYGONM * - Shapefile::SHAPE_TYPE_MULTIPOINTM * * @return self Returns $this to provide a fluent interface. */ protected function setShapeType($type) { if ($this->shape_type !== null) { throw new ShapefileException(Shapefile::ERR_SHP_TYPE_ALREADY_SET); } if (!isset(Shapefile::$shape_types[$type])) { throw new ShapefileException(Shapefile::ERR_SHP_TYPE_NOT_SUPPORTED, $type); } $this->shape_type = $type; return $this; } /** * Gets Shapefile base type, regardless of Z and M dimensions. * * @return int */ protected function getBasetype() { return $this->getShapeType(Shapefile::FORMAT_INT) % 10; } /** * Overwrites computed bounding box for the Shapefile. * No check is carried out except a formal compliance of dimensions. * * @param array $bounding_box Associative array with the xmin, xmax, ymin, ymax and optional zmin, zmax, mmin, mmax values. * * @return self Returns $this to provide a fluent interface. */ protected function overwriteComputedBoundingBox($bounding_box) { if ($bounding_box) { $this->computed_bounding_box = $this->sanitizeBoundingBox($bounding_box); } return $this; } /** * Sets a custom bounding box for the Shapefile. * No check is carried out except a formal compliance of dimensions. * * @param array $bounding_box Associative array with the xmin, xmax, ymin, ymax and optional zmin, zmax, mmin, mmax values. * * @return self Returns $this to provide a fluent interface. */ protected function setCustomBoundingBox($bounding_box) { $this->custom_bounding_box = $this->sanitizeBoundingBox($bounding_box); return $this; } /** * Resets custom bounding box for the Shapefile. * It will cause getBoundingBox() method to return a normally computed bbox instead of a custom one. * * @return self Returns $this to provide a fluent interface. */ protected function resetCustomBoundingBox() { $this->custom_bounding_box = null; return $this; } /** * Sets PRJ well-known-text. * * @param string $prj PRJ well-known-text. * Pass a falsy value (eg. false or "") to delete it. * * @return self Returns $this to provide a fluent interface. */ protected function setPRJ($prj) { $this->prj = $prj ?: null; return $this; } /** * Sets current total number of records. * * @param int $tot_records Total number of records currently in the files. * * @return self Returns $this to provide a fluent interface. */ protected function setTotRecords($tot_records) { $this->tot_records = $tot_records; return $this; } /** * Gets the state of the initialized flag. * * @return bool */ protected function isInitialized() { return $this->flag_initialized; } /** * Sets the state of the initialized flag. * * @param bool $value * * @return self Returns $this to provide a fluent interface. */ protected function setFlagInitialized($value) { $this->flag_initialized = $value; return $this; } /** * Adds a field to the shapefile definition. * Returns the effective field name after eventual sanitization. * * @param string $name Name of the field. Invalid names will be sanitized * (maximum 10 characters, only letters, numbers and underscores are allowed). * @param string $type Type of the field. It can be on of the following: * - Shapefile::DBF_TYPE_CHAR * - Shapefile::DBF_TYPE_DATE * - Shapefile::DBF_TYPE_LOGICAL * - Shapefile::DBF_TYPE_MEMO * - Shapefile::DBF_TYPE_NUMERIC * - Shapefile::DBF_TYPE_FLOAT * @param int $size Lenght of the field, depending on the type. * @param int $decimals Optional number of decimal digits for numeric type. * * @return string */ protected function addField($name, $type, $size, $decimals) { // Check init if ($this->isInitialized()) { throw new ShapefileException(Shapefile::ERR_SHP_FILE_ALREADY_INITIALIZED); } // Check filed count if (count($this->fields) >= Shapefile::DBF_MAX_FIELD_COUNT) { throw new ShapefileException(Shapefile::ERR_DBF_MAX_FIELD_COUNT_REACHED, Shapefile::DBF_MAX_FIELD_COUNT); } // Sanitize name and normalize case $name = $this->normalizeDBFFieldNameCase($this->sanitizeDBFFieldName($name)); // Check type if ( $type !== Shapefile::DBF_TYPE_CHAR && $type !== Shapefile::DBF_TYPE_DATE && $type !== Shapefile::DBF_TYPE_LOGICAL && $type !== Shapefile::DBF_TYPE_MEMO && $type !== Shapefile::DBF_TYPE_NUMERIC && $type !== Shapefile::DBF_TYPE_FLOAT ) { throw new ShapefileException(Shapefile::ERR_DBF_FIELD_TYPE_NOT_VALID, "$name - $type"); } // Check size $size = intval($size); $max_size = $this->getOption(Shapefile::OPTION_DBF_ALLOW_FIELD_SIZE_255) ? 255 : 254; if ( ($size < 1) || ($type == Shapefile::DBF_TYPE_CHAR && $size > $max_size) || ($type == Shapefile::DBF_TYPE_DATE && $size !== 8) || ($type == Shapefile::DBF_TYPE_LOGICAL && $size !== 1) || ($type == Shapefile::DBF_TYPE_MEMO && $size !== 10) || ($type == Shapefile::DBF_TYPE_NUMERIC && $size > $max_size) || ($type == Shapefile::DBF_TYPE_FLOAT && $size > $max_size) ) { throw new ShapefileException(Shapefile::ERR_DBF_FIELD_SIZE_NOT_VALID, "$name - $type - $size"); } // Minimal decimal formal check $decimals = intval($decimals); if ( ($type != Shapefile::DBF_TYPE_NUMERIC && $type != Shapefile::DBF_TYPE_FLOAT && $decimals !== 0) || ($type == Shapefile::DBF_TYPE_FLOAT && $decimals === 0) || ($decimals < 0) || ($decimals > 0 && $size - 1 <= $decimals) ) { throw new ShapefileException(Shapefile::ERR_DBF_FIELD_DECIMALS_NOT_VALID, "$name - $type - $decimals"); } // Add field $this->fields[$name] = [ 'type' => $type, 'size' => $size, 'decimals' => $decimals, ]; return $name; } /** * Normalize field name case according to OPTION_DBF_FORCE_ALL_CAPS status. * * @param string $input Field name to be case-normalized. * * @return string */ protected function normalizeDBFFieldNameCase($input) { return $this->getOption(Shapefile::OPTION_DBF_FORCE_ALL_CAPS) ? strtoupper($input) : $input; } /** * Pairs a Geometry with the Shapefile. * It enforces the Geometry type and computes Shapefile bounding box. * After that the Shapefile will be considered as "initialized" and no changes will be allowd to its structure. * * @param \Shapefile\Geometry\Geometry $Geometry Geometry to pair with. * * @return self Returns $this to provide a fluent interface. */ protected function pairGeometry(Geometry\Geometry $Geometry) { // Geometry type if ( $this->getBasetype() !== $Geometry->getSHPBasetype() || (!$Geometry->isEmpty() && $Geometry->isZ() !== $this->isZ() && !$this->getOption(Shapefile::OPTION_SUPPRESS_Z)) || (!$Geometry->isEmpty() && $Geometry->isM() !== $this->isM() && !$this->getOption(Shapefile::OPTION_SUPPRESS_M)) ) { throw new ShapefileException(Shapefile::ERR_SHP_GEOMETRY_TYPE_NOT_COMPATIBLE, $this->getShapeType(Shapefile::FORMAT_INT) . ' - ' . $this->getShapeType(Shapefile::FORMAT_STR)); } // Bounding box $bbox = $Geometry->getBoundingBox(); if (!$this->computed_bounding_box && $bbox) { if ($this->getOption(Shapefile::OPTION_SUPPRESS_Z)) { unset($bbox['zmin'], $bbox['zmax']); } if ($this->getOption(Shapefile::OPTION_SUPPRESS_M)) { unset($bbox['mmin'], $bbox['mmax']); } $this->computed_bounding_box = $bbox; } elseif ($bbox) { if ($bbox['xmin'] < $this->computed_bounding_box['xmin']) { $this->computed_bounding_box['xmin'] = $bbox['xmin']; } if ($bbox['xmax'] > $this->computed_bounding_box['xmax']) { $this->computed_bounding_box['xmax'] = $bbox['xmax']; } if ($bbox['ymin'] < $this->computed_bounding_box['ymin']) { $this->computed_bounding_box['ymin'] = $bbox['ymin']; } if ($bbox['ymax'] > $this->computed_bounding_box['ymax']) { $this->computed_bounding_box['ymax'] = $bbox['ymax']; } if ($this->isZ() && !$this->getOption(Shapefile::OPTION_SUPPRESS_Z)) { if ($bbox['zmin'] < $this->computed_bounding_box['zmin']) { $this->computed_bounding_box['zmin'] = $bbox['zmin']; } if ($bbox['zmax'] > $this->computed_bounding_box['zmax']) { $this->computed_bounding_box['zmax'] = $bbox['zmax']; } } if ($this->isM() && !$this->getOption(Shapefile::OPTION_SUPPRESS_M)) { if ($this->computed_bounding_box['mmin'] === false || $bbox['mmin'] < $this->computed_bounding_box['mmin']) { $this->computed_bounding_box['mmin'] = $bbox['mmin']; } if ($this->computed_bounding_box['mmax'] === false || $bbox['mmax'] > $this->computed_bounding_box['mmax']) { $this->computed_bounding_box['mmax'] = $bbox['mmax']; } } } // Mark Shapefile as initialized $this->setFlagInitialized(true); return $this; } /////////////////////////////// PRIVATE /////////////////////////////// /** * Checks formal compliance of a bounding box dimensions. * * @param array $bounding_box Associative array with the xmin, xmax, ymin, ymax and optional zmin, zmax, mmin, mmax values. */ private function sanitizeBoundingBox($bounding_box) { $bounding_box = array_intersect_key($bounding_box, array_flip(['xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax', 'mmin', 'mmax'])); if ($this->getOption(Shapefile::OPTION_SUPPRESS_Z)) { unset($bounding_box['zmin'], $bounding_box['zmax']); } if ($this->getOption(Shapefile::OPTION_SUPPRESS_M)) { unset($bounding_box['mmin'], $bounding_box['mmax']); } if ( !isset($bounding_box['xmin'], $bounding_box['xmax'], $bounding_box['ymin'], $bounding_box['ymax']) || ( ($this->isZ() && !$this->getOption(Shapefile::OPTION_SUPPRESS_Z) && !isset($bounding_box['zmin'], $bounding_box['zmax'])) || (!$this->isZ() && (isset($bounding_box['zmin']) || isset($bounding_box['zmax']))) ) || ( ($this->isM() && !$this->getOption(Shapefile::OPTION_SUPPRESS_M) && !isset($bounding_box['mmin'], $bounding_box['mmax'])) || (!$this->isM() && (isset($bounding_box['mmin']) || isset($bounding_box['mmax']))) ) ) { throw new ShapefileException(Shapefile::ERR_SHP_MISMATCHED_BBOX); } return $bounding_box; } /** * Returns a valid name for a DBF field. * * Only letters, numbers and underscores are allowed, everything else is converted to underscores. * Field names get truncated to 10 characters and conflicting ones are truncated to 8 characters adding a number from 1 to 99. * * @param string $input Raw name to be sanitized. * * @return string */ private function sanitizeDBFFieldName($input) { if ($input === '') { return $input; } $ret = substr(preg_replace('/[^a-zA-Z0-9]/', '_', $input), 0, 10); $fieldnames = array_fill_keys(array_keys(array_change_key_case($this->fields, CASE_UPPER)), true); if (isset($fieldnames[strtoupper($ret)])) { $ret = substr($ret, 0, 8) . '_1'; while (isset($fieldnames[strtoupper($ret)])) { $n = intval(trim(substr($ret, -2), '_')) + 1; if ($n > 99) { throw new ShapefileException(Shapefile::ERR_DBF_FIELD_NAME_NOT_VALID, $input); } $ret = substr($ret, 0, -2) . str_pad($n, 2, '_', STR_PAD_LEFT); } } return $ret; } }