melder/vendor/Shapefile/ShapefileWriter.php

1175 lines
41 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;
/**
* ShapefileWriter class.
*/
class ShapefileWriter extends Shapefile
{
/** SHP pack methods hash */
private static $shp_pack_methods = [
Shapefile::SHAPE_TYPE_NULL => 'packNull',
Shapefile::SHAPE_TYPE_POINT => 'packPoint',
Shapefile::SHAPE_TYPE_POLYLINE => 'packPolyLine',
Shapefile::SHAPE_TYPE_POLYGON => 'packPolygon',
Shapefile::SHAPE_TYPE_MULTIPOINT => 'packMultiPoint',
];
/** Buffered file types */
private static $buffered_files = [
Shapefile::FILE_SHP,
Shapefile::FILE_SHX,
Shapefile::FILE_DBF,
Shapefile::FILE_DBT,
];
/**
* @var array File writing buffers.
*/
private $buffers = [];
/**
* @var int Buffered records count.
*/
private $buffered_record_count = 0;
/**
* @var int Current offset in SHP file and buffer (in 16-bit words).
* First 50 16-bit are reserved for file header.
*/
private $shp_current_offset = 50;
/**
* @var int Next available block in DBT file.
*/
private $dbt_next_available_block = 0;
/**
* @var bool Flag representing whether file headers have been initialized or not.
*/
private $flag_init_headers = false;
/////////////////////////////// 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 = [])
{
// Options
$this->initOptions([
Shapefile::OPTION_BUFFERED_RECORDS,
Shapefile::OPTION_CPG_ENABLE_FOR_DEFAULT_CHARSET,
Shapefile::OPTION_DBF_ALLOW_FIELD_SIZE_255,
Shapefile::OPTION_DBF_FORCE_ALL_CAPS,
Shapefile::OPTION_DBF_NULL_PADDING_CHAR,
Shapefile::OPTION_DBF_NULLIFY_INVALID_DATES,
Shapefile::OPTION_DELETE_EMPTY_FILES,
Shapefile::OPTION_ENFORCE_GEOMETRY_DATA_STRUCTURE,
Shapefile::OPTION_EXISTING_FILES_MODE,
Shapefile::OPTION_SUPPRESS_M,
Shapefile::OPTION_SUPPRESS_Z,
], $options);
// Open files
$this->openFiles($files, true);
// Init Buffers
$this->buffers = array_fill_keys(array_intersect(self::$buffered_files, array_keys($this->getFiles())), '');
// Mode overwrite
if ($this->getOption(Shapefile::OPTION_EXISTING_FILES_MODE) === Shapefile::MODE_OVERWRITE) {
foreach (array_keys($this->getFiles()) as $file_type) {
if ($this->getFileSize($file_type) > 0) {
$this->fileTruncate($file_type);
$this->setFilePointer($file_type, 0);
}
}
}
// Mode append
if ($this->getOption(Shapefile::OPTION_EXISTING_FILES_MODE) === Shapefile::MODE_APPEND && $this->getFileSize(Shapefile::FILE_SHP) > 0) {
// Open Shapefile in reading mode
$ShapefileReader = new ShapefileReader($this->getFiles(), [
Shapefile::OPTION_DBF_CONVERT_TO_UTF8 => false,
Shapefile::OPTION_DBF_FORCE_ALL_CAPS => $this->getOption(Shapefile::OPTION_DBF_FORCE_ALL_CAPS),
Shapefile::OPTION_DBF_IGNORED_FIELDS => [],
Shapefile::OPTION_IGNORE_SHAPEFILE_BBOX => false,
]);
// Shape type
$this->setShapeType($ShapefileReader->getShapeType(Shapefile::FORMAT_INT));
// PRJ
$this->setPRJ($ShapefileReader->getPRJ());
// Charset
$this->setCharset($ShapefileReader->getCharset());
// Bounding Box
$this->overwriteComputedBoundingBox($ShapefileReader->getBoundingBox());
// Fields
foreach ($ShapefileReader->getFields() as $name => $field) {
$this->addField($name, $field['type'], $field['size'], $field['decimals']);
}
// Next DBT available block
if ($this->isFileOpen(Shapefile::FILE_DBT) && $this->getFileSize(Shapefile::FILE_DBT) > 0) {
$this->dbt_next_available_block = ($this->getFileSize(Shapefile::FILE_DBT) / Shapefile::DBT_BLOCK_SIZE);
}
// Number of records
$this->setTotRecords($ShapefileReader->getTotRecords());
// Close Shapefile in reading mode
$ShapefileReader = null;
// Mark Shapefile as initialized if there are any records
if ($this->getTotRecords() > 0) {
$this->setFlagInitialized(true);
}
// Flag init headers
$this->flag_init_headers = true;
// SHP current offset (in 16-bit words)
$this->shp_current_offset = $this->getFileSize(Shapefile::FILE_SHP) / 2;
// Remove DBF EOF marker
$dbf_size_without_eof = $this->getFileSize(Shapefile::FILE_DBF) - 1;
$this->setFilePointer(Shapefile::FILE_DBF, $dbf_size_without_eof);
if ($this->readData(Shapefile::FILE_DBF, 1) === $this->packChar(Shapefile::DBF_EOF_MARKER)) {
$this->fileTruncate(Shapefile::FILE_DBF, $dbf_size_without_eof);
}
// Reset pointers
foreach (array_keys($this->getFiles()) as $file_type) {
$this->resetFilePointer($file_type);
}
}
}
/**
* Destructor.
*
* Finalizes open files.
* If files were NOT passed as stream resources, empty useless files will be removed.
*/
public function __destruct()
{
// Flush buffers
$this->writeBuffers();
// Write DBF EOF marker to buffer
$this->writeData(Shapefile::FILE_DBF, $this->packChar(Shapefile::DBF_EOF_MARKER));
// Try setting Shapefile as NULL SHAPE if it hasn't been initialized yet (no records written)
if (!$this->isInitialized()) {
try {
$this->setShapeType(Shapefile::SHAPE_TYPE_NULL);
} catch (ShapefileException $e) {
// Nothing.
}
}
// Set buffered file pointers to beginning of files
foreach (array_keys($this->buffers) as $file_type) {
$this->setFilePointer($file_type, 0);
}
// Write SHP, SHX, DBF and DBT headers to buffers
$this->bufferData(Shapefile::FILE_SHP, $this->packSHPOrSHXHeader($this->getFileSize(Shapefile::FILE_SHP)));
$this->bufferData(Shapefile::FILE_SHX, $this->packSHPOrSHXHeader($this->getFileSize(Shapefile::FILE_SHX)));
$this->bufferData(Shapefile::FILE_DBF, $this->packDBFHeader());
if ($this->dbt_next_available_block > 0) {
$this->bufferData(Shapefile::FILE_DBT, $this->packDBTHeader());
}
// Write buffers containing the headers
$this->writeBuffers();
// Reset buffered file pointers
foreach (array_keys($this->buffers) as $file_type) {
$this->resetFilePointer($file_type);
}
// Write PRJ file
if ($this->isFileOpen(Shapefile::FILE_PRJ)) {
$this->fileTruncate(Shapefile::FILE_PRJ);
$this->writeData(Shapefile::FILE_PRJ, $this->packString($this->getPRJ()));
}
// Write CPG file
if ($this->isFileOpen(Shapefile::FILE_CPG)) {
$this->fileTruncate(Shapefile::FILE_CPG);
if ($this->getCharset() !== Shapefile::DBF_DEFAULT_CHARSET || $this->getOption(Shapefile::OPTION_CPG_ENABLE_FOR_DEFAULT_CHARSET)) {
$this->writeData(Shapefile::FILE_CPG, $this->packString($this->getCharset()));
}
}
// Close files and delete empty ones
$this->closeFiles();
if ($this->getOption(Shapefile::OPTION_DELETE_EMPTY_FILES)) {
foreach ($this->getFilenames() as $filename) {
if (filesize($filename) === 0) {
unlink($filename);
}
}
}
}
public function setShapeType($type)
{
return parent::setShapeType($type);
}
public function setCustomBoundingBox($bounding_box)
{
return parent::setCustomBoundingBox($bounding_box);
}
public function resetCustomBoundingBox()
{
return parent::resetCustomBoundingBox();
}
public function setPRJ($prj)
{
return parent::setPRJ($prj);
}
public function addField($name, $type, $size, $decimals)
{
return parent::addField($name, $type, $size, $decimals);
}
/**
* Adds a char 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).
* Only letters, numbers and underscores are allowed.
* @param int $size Lenght of the field, between 1 and 254 characters. Defaults to 254.
*
* @return string
*/
public function addCharField($name, $size = 254)
{
return $this->addField($name, Shapefile::DBF_TYPE_CHAR, $size, 0);
}
/**
* Adds a date 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).
* Only letters, numbers and underscores are allowed.
*
* @return string
*/
public function addDateField($name)
{
return $this->addField($name, Shapefile::DBF_TYPE_DATE, 8, 0);
}
/**
* Adds a logical/boolean 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).
* Only letters, numbers and underscores are allowed.
*
* @return string
*/
public function addLogicalField($name)
{
return $this->addField($name, Shapefile::DBF_TYPE_LOGICAL, 1, 0);
}
/**
* Adds a memo 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).
* Only letters, numbers and underscores are allowed.
*
* @return string
*/
public function addMemoField($name)
{
return $this->addField($name, Shapefile::DBF_TYPE_MEMO, 10, 0);
}
/**
* Adds numeric 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).
* Only letters, numbers and underscores are allowed.
* @param int $size Lenght of the field, between 1 and 254 characters. Defaults to 10.
* @param int $decimals Optional number of decimal digits. Defaults to 0.
*
* @return string
*/
public function addNumericField($name, $size = 10, $decimals = 0)
{
return $this->addField($name, Shapefile::DBF_TYPE_NUMERIC, $size, $decimals);
}
/**
* Adds floating point 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).
* Only letters, numbers and underscores are allowed.
* @param int $size Lenght of the field, between 1 and 254 characters. Defaults to 20.
* @param int $decimals Number of decimal digits. Defaults to 10.
*
* @return string
*/
public function addFloatField($name, $size = 20, $decimals = 10)
{
return $this->addField($name, Shapefile::DBF_TYPE_FLOAT, $size, $decimals);
}
/**
* Writes a record to the Shapefile.
*
* @param \Shapefile\Geometry\Geometry $Geometry Geometry to write.
*
* @return self Returns $this to provide a fluent interface.
*/
public function writeRecord(Geometry\Geometry $Geometry)
{
// Init headers
if (!$this->flag_init_headers) {
$this->bufferData(Shapefile::FILE_SHP, $this->packNulPadding(Shapefile::SHP_HEADER_SIZE));
$this->bufferData(Shapefile::FILE_SHX, $this->packNulPadding(Shapefile::SHX_HEADER_SIZE));
$this->bufferData(Shapefile::FILE_DBF, $this->packNulPadding($this->getDBFHeaderSize()));
if (in_array(Shapefile::DBF_TYPE_MEMO, $this->arrayColumn($this->getFields(), 'type'))) {
if (!$this->isFileOpen(Shapefile::FILE_DBT)) {
throw new ShapefileException(Shapefile::ERR_FILE_MISSING, strtoupper(Shapefile::FILE_DBT));
}
$this->bufferData(Shapefile::FILE_DBT, $this->packNulPadding(Shapefile::DBT_BLOCK_SIZE));
++$this->dbt_next_available_block;
}
$this->flag_init_headers = true;
}
// Pair with Geometry
$this->pairGeometry($Geometry);
// Write data to temporary buffers to make sure no exceptions are raised within current record
$temp = $this->packSHPAndSHXData($Geometry) + $this->packDBFAndDBTData($Geometry);
// Write data to real buffers
foreach (array_keys($this->buffers) as $file_type) {
$this->bufferData($file_type, $temp[$file_type]);
}
$this->shp_current_offset = $temp['shp_current_offset'];
$this->dbt_next_available_block = $temp['dbt_next_available_block'];
$this->setTotRecords($this->getTotRecords() + 1);
++$this->buffered_record_count;
// Eventually flush buffers
$option_buffered_records = $this->getOption(Shapefile::OPTION_BUFFERED_RECORDS);
if ($option_buffered_records > 0 && $this->buffered_record_count == $option_buffered_records) {
$this->writeBuffers();
}
return $this;
}
/**
* Writes buffers to files.
*
* @return self Returns $this to provide a fluent interface.
*/
public function flushBuffer()
{
$this->writeBuffers();
return $this;
}
/////////////////////////////// PRIVATE ///////////////////////////////
/**
* Stores binary string packed data into a buffer.
*
* @param string $file_type File type.
* @param string $data String value to write.
*
* @return self Returns $this to provide a fluent interface.
*/
private function bufferData($file_type, $data)
{
$this->buffers[$file_type] .= $data;
return $this;
}
/**
* Writes buffers to files.
*
* @return self Returns $this to provide a fluent interface.
*/
private function writeBuffers()
{
foreach ($this->buffers as $file_type => $buffer) {
if ($buffer !== '') {
$this->writeData($file_type, $buffer);
$this->buffers[$file_type] = '';
}
}
$this->buffered_record_count = 0;
return $this;
}
/**
* Packs an unsigned char into binary string.
*
* @param string $data Value to pack.
*
* @return string
*/
private function packChar($data)
{
return pack('C', $data);
}
/**
* Packs an unsigned short, 16 bit, little endian byte order, into binary string.
*
* @param string $data Value to pack.
*
* @return string
*/
private function packInt16L($data)
{
return pack('v', $data);
}
/**
* Packs an unsigned long, 32 bit, big endian byte order, into binary string.
*
* @param string $data Value to pack.
*
* @return string
*/
private function packInt32B($data)
{
return pack('N', $data);
}
/**
* Packs an unsigned long, 32 bit, little endian byte order, into binary string.
*
* @param string $data Value to pack.
*
* @return string
*/
private function packInt32L($data)
{
return pack('V', $data);
}
/**
* Packs a double, 64 bit, little endian byte order, into binary string.
*
* @param string $data Value to pack.
*
* @return string
*/
private function packDoubleL($data)
{
$data = pack('d', $data);
if ($this->isBigEndianMachine()) {
$data = strrev($data);
}
return $data;
}
/**
* Packs a string into binary string.
*
* @param string $data Value to pack.
*
* @return string
*/
private function packString($data)
{
return pack('A*', $data);
}
/**
* Packs a NUL-padding of given length into binary string.
*
* @param string $lenght Length of the padding to pack.
*
* @return string
*/
private function packNulPadding($lenght)
{
return pack('a*', str_repeat("\0", $lenght));
}
/**
* Packs some XY coordinates into binary string.
*
* @param array $coordinates Array with "x" and "y" coordinates.
*
* @return string
*/
private function packXY($coordinates)
{
return $this->packDoubleL($coordinates['x'])
. $this->packDoubleL($coordinates['y']);
}
/**
* Packs a Z coordinate into binary string.
*
* @param array $coordinates Array with "z" coordinate.
*
* @return string
*/
private function packZ($coordinates)
{
return $this->packDoubleL($this->getOption(Shapefile::OPTION_SUPPRESS_Z) ? 0 : $coordinates['z']);
}
/**
* Packs an M coordinate into binary string.
*
* @param array $coordinates Array with "m" coordinate.
*
* @return string
*/
private function packM($coordinates)
{
return $this->packDoubleL($this->getOption(Shapefile::OPTION_SUPPRESS_M) ? 0 : $this->parseM($coordinates['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»
* This library uses bool value false to represent "no data".
*
* @param float|bool $value Value to parse.
*
* @return float
*/
private function parseM($value)
{
return ($value === false) ? Shapefile::SHP_NO_DATA_VALUE : $value;
}
/**
* Packs an XY bounding box into binary string.
*
* @param array $bounding_box Associative array with xmin, xmax, ymin, ymax values.
*
* @return string
*/
private function packXYBoundingBox($bounding_box)
{
return $this->packDoubleL($bounding_box['xmin'])
. $this->packDoubleL($bounding_box['ymin'])
. $this->packDoubleL($bounding_box['xmax'])
. $this->packDoubleL($bounding_box['ymax']);
}
/**
* Packs a Z range into binary string.
*
* @param array $bounding_box Associative array with zmin and zmax values.
*
* @return string
*/
private function packZRange($bounding_box)
{
return $this->packDoubleL($this->getOption(Shapefile::OPTION_SUPPRESS_Z) ? 0 : $bounding_box['zmin'])
. $this->packDoubleL($this->getOption(Shapefile::OPTION_SUPPRESS_Z) ? 0 : $bounding_box['zmax']);
}
/**
* Packs an M range into binary string.
*
* @param array $bounding_box Associative array with mmin and mmax values.
*
* @return string
*/
private function packMRange($bounding_box)
{
return $this->packDoubleL($this->getOption(Shapefile::OPTION_SUPPRESS_M) ? 0 : $this->parseM($bounding_box['mmin']))
. $this->packDoubleL($this->getOption(Shapefile::OPTION_SUPPRESS_M) ? 0 : $this->parseM($bounding_box['mmax']));
}
/**
* Packs a Null shape into binary string.
*
* @return string
*/
private function packNull()
{
// Shape type
return $this->packInt32L(Shapefile::SHAPE_TYPE_NULL);
}
/**
* Packs a Point, PointM or PointZ shape into binary string.
*
* @param \Shapefile\Geometry\Geometry $Geometry Geometry to pack.
*
* @return string
*/
private function packPoint(Geometry\Geometry $Geometry)
{
$array = $Geometry->getArray();
$bounding_box = $Geometry->getBoundingBox();
$is_m = $this->isM();
$is_z = $this->isZ();
$shape_type = $is_z ? Shapefile::SHAPE_TYPE_POINTZ : ($is_m ? Shapefile::SHAPE_TYPE_POINTM : Shapefile::SHAPE_TYPE_POINT);
// Shape type
$ret = $this->packInt32L($shape_type);
// XY Coordinates
$ret .= $this->packXY($array);
if ($is_z) {
// Z Coordinate
$ret .= $this->packZ($coordinates);
}
if ($is_m) {
// M Coordinate
$ret .= $this->packM($coordinates);
}
return $ret;
}
/**
* Packs a MultiPoint, MultiPointM or MultiPointZ shape into binary string.
*
* @param \Shapefile\Geometry\Geometry $Geometry Geometry to pack.
*
* @return string
*/
private function packMultiPoint(Geometry\Geometry $Geometry)
{
$array = $Geometry->getArray();
$bounding_box = $Geometry->getBoundingBox();
$is_m = $this->isM();
$is_z = $this->isZ();
$shape_type = $is_z ? Shapefile::SHAPE_TYPE_MULTIPOINTZ : ($is_m ? Shapefile::SHAPE_TYPE_MULTIPOINTM : Shapefile::SHAPE_TYPE_MULTIPOINT);
// Shape type
$ret = $this->packInt32L($shape_type);
// XY Bounding Box
$ret .= $this->packXYBoundingBox($bounding_box);
// NumPoints
$ret .= $this->packInt32L($array['numpoints']);
// Points
foreach ($array['points'] as $coordinates) {
$ret .= $this->packXY($coordinates);
}
if ($is_z) {
// Z Range
$ret .= $this->packZRange($bounding_box);
// Z Array
foreach ($array['points'] as $coordinates) {
$ret .= $this->packZ($coordinates);
}
}
if ($is_m) {
// M Range
$ret .= $this->packMRange($bounding_box);
// M Array
foreach ($array['points'] as $coordinates) {
$ret .= $this->packM($coordinates);
}
}
return $ret;
}
/**
* Packs a PolyLine, PolyLineM or PolyLineZ shape into binary string.
*
* @param \Shapefile\Geometry\Geometry $Geometry Geometry to pack.
* @param bool $flag_polygon Optional flag to pack Polygon shapes.
*
* @return string
*/
private function packPolyLine(Geometry\Geometry $Geometry, $flag_polygon = false)
{
$array = $Geometry->getArray();
$bounding_box = $Geometry->getBoundingBox();
$is_m = $this->isM();
$is_z = $this->isZ();
if ($flag_polygon) {
$shape_type = $is_z ? Shapefile::SHAPE_TYPE_POLYGONZ : ($is_m ? Shapefile::SHAPE_TYPE_POLYGONM : Shapefile::SHAPE_TYPE_POLYGON);
} else {
$shape_type = $is_z ? Shapefile::SHAPE_TYPE_POLYLINEZ : ($is_m ? Shapefile::SHAPE_TYPE_POLYLINEM : Shapefile::SHAPE_TYPE_POLYLINE);
}
// PolyLines and Polygons are always MultiLinestrings and MultiPolygons in Shapefiles
if (!isset($array['parts'])) {
$array = [
'numparts' => 1,
'parts' => [$array],
];
}
// Polygons need to be reduced as PolyLines
if ($flag_polygon) {
$parts = [];
foreach ($array['parts'] as $part) {
foreach ($part['rings'] as $ring) {
$parts[] = $ring;
}
}
$array = [
'numparts' => count($parts),
'parts' => $parts,
];
}
// Shape type
$ret = $this->packInt32L($shape_type);
// XY Bounding Box
$ret .= $this->packXYBoundingBox($bounding_box);
// NumParts
$ret .= $this->packInt32L($array['numparts']);
// NumPoints
$ret .= $this->packInt32L(array_sum($this->arrayColumn($array['parts'], 'numpoints')));
// Parts
$part_first_index = 0;
foreach ($array['parts'] as $part) {
$ret .= $this->packInt32L($part_first_index);
$part_first_index += $part['numpoints'];
}
// Points
foreach ($array['parts'] as $part) {
foreach ($part['points'] as $coordinates) {
$ret .= $this->packXY($coordinates);
}
}
if ($is_z) {
// Z Range
$ret .= $this->packZRange($bounding_box);
// Z Array
foreach ($array['parts'] as $part) {
foreach ($part['points'] as $coordinates) {
$ret .= $this->packZ($coordinates);
}
}
}
if ($is_m) {
// M Range
$ret .= $this->packMRange($bounding_box);
// M Array
foreach ($array['parts'] as $part) {
foreach ($part['points'] as $coordinates) {
$ret .= $this->packM($coordinates);
}
}
}
return $ret;
}
/**
* Packs a Polygon, PolygonM or PolygonZ shape into binary string.
* It forces closed rings and clockwise orientation in order to comply with ESRI Shapefile specifications.
*
* @param \Shapefile\Geometry\Geometry $Geometry Geometry to pack.
*
* @return string
*/
private function packPolygon(Geometry\Geometry $Geometry)
{
$Geometry->forceClosedRings();
$Geometry->forceClockwise();
return $this->packPolyLine($Geometry, true);
}
/**
* Packs SHP and SHX data from a Geometry object into binary strings and returns an array with SHP, SHX and "shp_current_offset" members.
*
* @param \Shapefile\Geometry\Geometry $Geometry Input Geometry.
*
* @return array
*/
private function packSHPAndSHXData(Geometry\Geometry $Geometry)
{
// Pack Geometry data
$method = self::$shp_pack_methods[$Geometry->isEmpty() ? Shapefile::SHAPE_TYPE_NULL : $this->getBasetype()];
$shp_data = $this->{$method}($Geometry);
// Compute content lenght in 16-bit words
$shp_content_length = strlen($shp_data) / 2;
return [
Shapefile::FILE_SHP => $this->packInt32B($this->getTotRecords() + 1)
. $this->packInt32B($shp_content_length)
. $shp_data,
Shapefile::FILE_SHX => $this->packInt32B($this->shp_current_offset)
. $this->packInt32B($shp_content_length),
'shp_current_offset' => $this->shp_current_offset + $shp_content_length + 4,
];
}
/**
* Packs DBF and DBT data from a Geometry object into binary strings and returns an array with SHP, DBT and "dbt_next_available_block" members.
*
* @param \Shapefile\Geometry\Geometry $Geometry Input Geometry.
*
* @return array
*/
private function packDBFAndDBTData(Geometry\Geometry $Geometry)
{
$ret = [
Shapefile::FILE_DBF => '',
Shapefile::FILE_DBT => '',
'dbt_next_available_block' => $this->dbt_next_available_block,
];
// Deleted flag
$ret[Shapefile::FILE_DBF] = $this->packChar($Geometry->isDeleted() ? Shapefile::DBF_DELETED_MARKER : Shapefile::DBF_BLANK);
// Data
$data = $Geometry->getDataArray();
if ($this->getOption(Shapefile::OPTION_DBF_FORCE_ALL_CAPS)) {
$data = array_change_key_case($data, CASE_UPPER);
}
foreach ($this->getFields() as $name => $field) {
if (!array_key_exists($name, $data)) {
if ($this->getOption(Shapefile::OPTION_ENFORCE_GEOMETRY_DATA_STRUCTURE)) {
throw new ShapefileException(Shapefile::ERR_GEOM_MISSING_FIELD, $name);
}
$data[$name] = null;
}
$value = $this->encodeFieldValue($field['type'], $field['size'], $field['decimals'], $data[$name]);
// Memo (DBT)
if ($field['type'] == Shapefile::DBF_TYPE_MEMO && $value !== null) {
$dbt = $this->packDBTData($value, $field['size']);
$value = str_pad($ret['dbt_next_available_block'], $field['size'], chr(Shapefile::DBF_BLANK), STR_PAD_LEFT);
$ret[Shapefile::FILE_DBT] .= $dbt['data'];
$ret['dbt_next_available_block'] += $dbt['blocks'];
}
// Null
if ($value === null) {
$value = str_repeat(($this->getOption(Shapefile::OPTION_DBF_NULL_PADDING_CHAR) !== null ? $this->getOption(Shapefile::OPTION_DBF_NULL_PADDING_CHAR) : chr(Shapefile::DBF_BLANK)), $field['size']);
}
// Add packed value to temp buffer
$ret[Shapefile::FILE_DBF] .= $this->packString($value);
}
return $ret;
}
/**
* Packs DBT data into a binary string and return an array with "blocks" and "data" members.
*
* @param string $data Data to write
* @param int $field_size Size of the DBF field.
*
* @return array
*/
private function packDBTData($data, $field_size)
{
$ret = [
'blocks' => 0,
'data' => '',
];
// Ignore empty values
if ($data === '') {
$ret['data'] = str_repeat(chr(Shapefile::DBF_BLANK), $field_size);
} else {
// Corner case: there's not enough space at the end of the last block for 2 field terminators. Add a space and switch to the next block!
if (strlen($data) % Shapefile::DBT_BLOCK_SIZE == Shapefile::DBT_BLOCK_SIZE - 1) {
$data .= chr(Shapefile::DBF_BLANK);
}
// Add TWO field terminators
$data .= str_repeat(chr(Shapefile::DBT_FIELD_TERMINATOR), 2);
// Write data to DBT buffer
foreach (str_split($data, Shapefile::DBT_BLOCK_SIZE) as $block) {
$ret['blocks'] += 1;
$ret['data'] .= $this->packString(str_pad($block, Shapefile::DBT_BLOCK_SIZE, "\0", STR_PAD_RIGHT));
}
}
return $ret;
}
/**
* Encodes a value to be written into a DBF field as a raw string.
*
* @param string $type Type of the field.
* @param int $size Lenght of the field.
* @param int $decimals Number of decimal digits for numeric type.
* @param string $value Value to encode.
*
* @return string|null
*/
private function encodeFieldValue($type, $size, $decimals, $value)
{
switch ($type) {
case Shapefile::DBF_TYPE_CHAR:
if ($value !== null) {
$value = $this->truncateOrPadString($value, $size);
}
break;
case Shapefile::DBF_TYPE_DATE:
if (is_a($value, 'DateTime')) {
$value = $value->format('Ymd');
} elseif ($value !== null) {
// Try YYYY-MM-DD format
$DateTime = \DateTime::createFromFormat('Y-m-d', $value);
$errors = \DateTime::getLastErrors();
if ($errors['warning_count'] || $errors['error_count']) {
// Try YYYYMMDD format
$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 : $this->truncateOrPadString($this->sanitizeNumber($value), $size);
} else {
$value = $DateTime->format('Ymd');
}
}
break;
case Shapefile::DBF_TYPE_LOGICAL:
if ($value === null) {
$value = Shapefile::DBF_VALUE_NULL;
} elseif ($value === true || strpos(Shapefile::DBF_VALUE_MASK_TRUE, substr(trim($value), 0, 1)) !== false) {
$value = Shapefile::DBF_VALUE_TRUE;
} else {
$value = Shapefile::DBF_VALUE_FALSE;
}
break;
case Shapefile::DBF_TYPE_MEMO:
if ($value !== null) {
$value = (string) $value;
}
break;
case Shapefile::DBF_TYPE_NUMERIC:
case Shapefile::DBF_TYPE_FLOAT:
if ($value !== null) {
if (is_string($value)) {
$value = trim($value);
$flag_negative = substr($value, 0, 1) === '-';
$intpart = $this->sanitizeNumber(strpos($value, '.') === false ? $value : strstr($value, '.', true));
$decpart = $this->sanitizeNumber(substr(strstr($value, '.', false), 1));
$decpart = strlen($decpart) > $decimals ? substr($decpart, 0, $decimals) : str_pad($decpart, $decimals, '0', STR_PAD_RIGHT);
$value = ($flag_negative ? '-' : '') . $intpart . ($decimals > 0 ? '.' : '') . $decpart;
} else {
$value = number_format(floatval($value), $decimals, '.', '');
}
if (strlen($value) > $size) {
throw new ShapefileException(Shapefile::ERR_INPUT_NUMERIC_VALUE_OVERFLOW, "value:$intpart - size:($size.$decimals)");
}
$value = str_pad($value, $size, chr(Shapefile::DBF_BLANK), STR_PAD_LEFT);
}
break;
}
return $value;
}
/**
* Truncates long input strings and right-pads short ones to maximum/minimun lenght.
*
* @param string $value Value to pad.
* @param int $size Lenght of the field.
*
* @return string
*/
private function truncateOrPadString($value, $size)
{
return str_pad(substr($value, 0, $size), $size, chr(Shapefile::DBF_BLANK), STR_PAD_RIGHT);
}
/**
* Removes illegal characters from a numeric string.
*
* @param string $value Value to sanitize.
*
* @return string
*/
private function sanitizeNumber($value)
{
return preg_replace('/[^0-9]/', '', $value);
}
/**
* Packs SHP or SHX file header into a binary string.
*
* @param int $file_size File size in bytes.
*
* @return string
*/
private function packSHPOrSHXHeader($file_size)
{
$ret = '';
// File Code
$ret .= $this->packInt32B(Shapefile::SHP_FILE_CODE);
// Unused bytes
$ret .= $this->packNulPadding(20);
// File Length (in 16-bit words)
$ret .= $this->packInt32B($file_size / 2);
// Version
$ret .= $this->packInt32L(Shapefile::SHP_VERSION);
// Shape Type
$ret .= $this->packInt32L($this->getShapeType(Shapefile::FORMAT_INT));
//Bounding Box
$bounding_box = $this->getBoundingBox();
$ret .= $this->packXYBoundingBox($bounding_box);
$ret .= $this->packZRange($this->isZ() ? $bounding_box : ['zmin' => 0, 'zmax' => 0]);
$ret .= $this->packMRange($this->isM() ? $bounding_box : ['mmin' => 0, 'mmax' => 0]);
return $ret;
}
/**
* Packs DBF file header into a binary string.
*
* @return string
*/
private function packDBFHeader()
{
$ret = '';
// Version number
$ret .= $this->packChar($this->dbt_next_available_block > 0 ? Shapefile::DBF_VERSION_WITH_DBT : Shapefile::DBF_VERSION);
// Date of last update
$ret .= $this->packChar(intval(date('Y')) - 1900);
$ret .= $this->packChar(intval(date('m')));
$ret .= $this->packChar(intval(date('d')));
// Number of records
$ret .= $this->packInt32L($this->getTotRecords());
// Header size
$ret .= $this->packInt16L($this->getDBFHeaderSize());
// Record size
$ret .= $this->packInt16L($this->getDBFRecordSize());
// Reserved bytes
$ret .= $this->packNulPadding(20);
// Field descriptor array
foreach ($this->getFields() as $name => $field) {
// Name
$ret .= $this->packString(str_pad($name, 10, "\0", STR_PAD_RIGHT));
$ret .= $this->packNulPadding(1);
// Type
$ret .= $this->packString($field['type']);
$ret .= $this->packNulPadding(4);
// Size
$ret .= $this->packChar($field['size']);
// Decimals
$ret .= $this->packChar($field['decimals']);
$ret .= $this->packNulPadding(14);
}
// Field terminator
$ret .= $this->packChar(Shapefile::DBF_FIELD_TERMINATOR);
return $ret;
}
/**
* Packs DBT file header into a binary string.
*
* @return string
*/
private function packDBTHeader()
{
$ret = '';
// Next available block
$ret .= $this->packInt32L($this->dbt_next_available_block);
// Reserved bytes
$ret .= $this->packNulPadding(12);
// Version number
$ret .= $this->packChar(Shapefile::DBF_VERSION);
return $ret;
}
/**
* Computes DBF header size.
* 32bytes + (number of fields x 32) + 1 (field terminator character).
*
* @return int
*/
private function getDBFHeaderSize()
{
return 33 + (32 * count($this->getFields()));
}
/**
* Computes DBF record size.
* Sum of all fields sizes + 1 (record deleted flag).
*
* @return int
*/
private function getDBFRecordSize()
{
return array_sum($this->arrayColumn($this->getFields(), 'size')) + 1;
}
/**
* Substitute for PHP 5.5 array_column() function.
*
* @param array $array Multidimensional array.
* @param string $key Key of the column to return.
*
* @return array
*/
private function arrayColumn($array, $key)
{
return array_map(function ($element) use ($key) {
return $element[$key];
}, $array);
}
}