1<?php 2 3/** 4 * This file contains the BinaryReader class. 5 * For more information see the class description below. 6 * 7 * @author Peter Bathory <peter.bathory@cartographia.hu> 8 * @since 2016-02-18 9 * 10 * This code is open-source and licenced under the Modified BSD License. 11 * For the full copyright and license information, please view the LICENSE 12 * file that was distributed with this source code. 13 */ 14 15namespace geoPHP\Adapter; 16 17use geoPHP\Geometry\Collection; 18use geoPHP\Geometry\Geometry; 19use geoPHP\Geometry\GeometryCollection; 20use geoPHP\Geometry\LineString; 21use geoPHP\Geometry\MultiGeometry; 22use geoPHP\Geometry\MultiLineString; 23use geoPHP\Geometry\MultiPoint; 24use geoPHP\Geometry\MultiPolygon; 25use geoPHP\Geometry\Point; 26use geoPHP\Geometry\Polygon; 27 28/** 29 * PHP Geometry <-> TWKB encoder/decoder 30 * 31 * "Tiny Well-known Binary is is a multi-purpose format for serializing vector geometry data into a byte buffer, 32 * with an emphasis on minimizing size of the buffer." 33 * @see https://github.com/TWKB/Specification/blob/master/twkb.md 34 * 35 * This implementation supports: 36 * - reading and writing all geometry types (1-7) 37 * - empty geometries 38 * - extended precision (Z, M coordinates; custom precision) 39 * Partially supports: 40 * - bounding box: can read and write, but don't store readed boxes (API missing) 41 * - size attribute: can read and write size attribute, but seeking is not supported 42 * - ID list: can read and write, but API is completely missing 43 */ 44class TWKB implements GeoAdapter 45{ 46 47 protected $writeOptions = [ 48 'decimalDigitsXY' => 5, 49 'decimalDigitsZ' => 0, 50 'decimalDigitsM' => 0, 51 'includeSize' => false, 52 'includeBoundingBoxes' => false, 53 ]; 54 55 /** @var Point|null */ 56 private $lastPoint = null; 57 58 /** @var BinaryReader $reader */ 59 private $reader; 60 61 /** @var BinaryWriter $writer */ 62 private $writer; 63 64 /** @var array Maps Geometry types to TWKB type codes */ 65 protected static $typeMap = [ 66 Geometry::POINT => 1, 67 Geometry::LINE_STRING => 2, 68 Geometry::POLYGON => 3, 69 Geometry::MULTI_POINT => 4, 70 Geometry::MULTI_LINE_STRING => 5, 71 Geometry::MULTI_POLYGON => 6, 72 Geometry::GEOMETRY_COLLECTION => 7 73 ]; 74 75 /** 76 * Read TWKB into geometry objects 77 * 78 * @param string $twkb Tiny Well-known-binary string 79 * @param bool $isHexString If this is a hexadecimal string that is in need of packing 80 * 81 * @return Geometry 82 * 83 * @throws \Exception 84 */ 85 public function read($twkb, $isHexString = false) 86 { 87 if ($isHexString) { 88 $twkb = @pack('H*', $twkb); 89 } 90 91 if (empty($twkb)) { 92 throw new \Exception('Cannot read empty TWKB. Found ' . gettype($twkb)); 93 } 94 95 $this->reader = new BinaryReader($twkb); 96 97 $geometry = $this->getGeometry(); 98 99 $this->reader->close(); 100 101 return $geometry; 102 } 103 104 protected function getGeometry() 105 { 106 $options = []; 107 $type = $this->reader->readUInt8(); 108 $metadataHeader = $this->reader->readUInt8(); 109 110 $geometryType = $type & 0x0F; 111 $options['precision'] = BinaryReader::zigZagDecode($type >> 4); 112 $options['precisionFactor'] = pow(10, $options['precision']); 113 114 $options['hasBoundingBox'] = ($metadataHeader >> 0 & 1) == 1; 115 $options['hasSizeAttribute'] = ($metadataHeader >> 1 & 1) == 1; 116 $options['hasIdList'] = ($metadataHeader >> 2 & 1) == 1; 117 $options['hasExtendedPrecision'] = ($metadataHeader >> 3 & 1) == 1; 118 $options['isEmpty'] = ($metadataHeader >> 4 & 1) == 1; 119 $options['unused1'] = ($metadataHeader >> 5 & 1) == 1; 120 $options['unused2'] = ($metadataHeader >> 6 & 1) == 1; 121 $options['unused3'] = ($metadataHeader >> 7 & 1) == 1; 122 123 if ($options['hasExtendedPrecision']) { 124 $extendedPrecision = $this->reader->readUInt8(); 125 126 $options['hasZ'] = ($extendedPrecision & 0x01) === 0x01; 127 $options['hasM'] = ($extendedPrecision & 0x02) === 0x02; 128 129 $options['zPrecision'] = ($extendedPrecision & 0x1C) >> 2; 130 $options['zPrecisionFactor'] = pow(10, $options['zPrecision']); 131 132 $options['mPrecision'] = ($extendedPrecision & 0xE0) >> 5; 133 $options['mPrecisionFactor'] = pow(10, $options['mPrecision']); 134 } else { 135 $options['hasZ'] = false; 136 $options['hasM'] = false; 137 } 138 if ($options['hasSizeAttribute']) { 139 $options['remainderSize'] = $this->reader->readUVarInt(); 140 } 141 if ($options['hasBoundingBox']) { 142 $dimension = 2 + ($options['hasZ'] ? 1 : 0) + ($options['hasM'] ? 1 : 0); 143 $precisions = [ 144 $options['precisionFactor'], 145 $options['precisionFactor'], 146 $options['hasZ'] ? $options['zPrecisionFactor'] : 0, 147 $options['hasM'] ? $options['mPrecisionFactor'] : 0 148 ]; 149 $bBoxMin = $bBoxMax = []; 150 for ($i = 0; $i < $dimension; $i++) { 151 $bBoxMin[$i] = $this->reader->readUVarInt() / $precisions[$i]; 152 $bBoxMax[$i] = $this->reader->readUVarInt() / $precisions[$i] + $bBoxMin[$i]; 153 } 154 /** @noinspection PhpUndefinedVariableInspection (minimum 2 dimension) */ 155 $options['boundingBox'] = ['minXYZM' => $bBoxMin, 'maxXYZM' => $bBoxMax]; 156 } 157 158 if ($options['unused1']) { 159 $this->reader->readUVarInt(); 160 } 161 if ($options['unused2']) { 162 $this->reader->readUVarInt(); 163 } 164 if ($options['unused3']) { 165 $this->reader->readUVarInt(); 166 } 167 168 $this->lastPoint = new Point(0, 0, 0, 0); 169 170 switch ($geometryType) { 171 case 1: 172 $geometry = $this->getPoint($options); 173 break; 174 case 2: 175 $geometry = $this->getLineString($options); 176 break; 177 case 3: 178 $geometry = $this->getPolygon($options); 179 break; 180 case 4: 181 $geometry = $this->getMulti('Point', $options); 182 break; 183 case 5: 184 $geometry = $this->getMulti('LineString', $options); 185 break; 186 case 6: 187 $geometry = $this->getMulti('Polygon', $options); 188 break; 189 case 7: 190 $geometry = $this->getMulti('Geometry', $options); 191 break; 192 default: 193 throw new \Exception( 194 'Geometry type ' . $geometryType . 195 ' (' . (array_search($geometryType, self::$typeMap) ?: 'unknown') . ') not supported' 196 ); 197 } 198 199 return $geometry; 200 } 201 202 /** 203 * @param array $options 204 * 205 * @return Point 206 * @throws \Exception 207 */ 208 protected function getPoint($options) 209 { 210 if ($options['isEmpty']) { 211 return new Point(); 212 } 213 $x = round( 214 $this->lastPoint->x() + $this->reader->readSVarInt() / $options['precisionFactor'], 215 $options['precision'] 216 ); 217 $y = round( 218 $this->lastPoint->y() + $this->reader->readSVarInt() / $options['precisionFactor'], 219 $options['precision'] 220 ); 221 $z = $options['hasZ'] ? round( 222 $this->lastPoint->z() + $this->reader->readSVarInt() / $options['zPrecisionFactor'], 223 $options['zPrecision'] 224 ) : null; 225 $m = $options['hasM'] ? round( 226 $this->lastPoint->m() + $this->reader->readSVarInt() / $options['mPrecisionFactor'], 227 $options['mPrecision'] 228 ) : null; 229 230 $this->lastPoint = new Point($x, $y, $z, $m); 231 return $this->lastPoint; 232 } 233 234 /** 235 * @param array $options 236 * 237 * @return LineString 238 * @throws \Exception 239 */ 240 protected function getLineString($options) 241 { 242 if ($options['isEmpty']) { 243 return new LineString(); 244 } 245 246 $pointCount = $this->reader->readUVarInt(); 247 248 $points = []; 249 for ($i = 0; $i < $pointCount; $i++) { 250 $points[] = $this->getPoint($options); 251 } 252 253 return new LineString($points); 254 } 255 256 /** 257 * @param array $options 258 * 259 * @return Polygon 260 * @throws \Exception 261 */ 262 protected function getPolygon($options) 263 { 264 if ($options['isEmpty']) { 265 return new Polygon(); 266 } 267 268 $ringCount = $this->reader->readUVarInt(); 269 270 $rings = []; 271 for ($i = 0; $i < $ringCount; $i++) { 272 $rings[] = $this->getLineString($options); 273 } 274 275 return new Polygon($rings, true); 276 } 277 278 /** 279 * @param string $type 280 * @param array $options 281 * 282 * @return MultiGeometry|null 283 * @throws \Exception 284 */ 285 protected function getMulti($type, $options) 286 { 287 $multiLength = $this->reader->readUVarInt(); 288 289 if ($options['hasIdList']) { 290 for ($i = 0; $i < $multiLength; $i++) { 291 $idList[] = $this->reader->readSVarInt(); 292 } 293 } 294 295 $components = []; 296 for ($i = 0; $i < $multiLength; $i++) { 297 if ($type !== 'Geometry') { 298 $func = 'get' . $type; 299 $components[] = $this->$func($options); 300 } else { 301 $components[] = $this->getGeometry(); 302 } 303 } 304 switch ($type) { 305 case 'Point': 306 return new MultiPoint($components); 307 case 'LineString': 308 return new MultiLineString($components); 309 case 'Polygon': 310 return new MultiPolygon($components); 311 case 'Geometry': 312 return new GeometryCollection($components); 313 } 314 return null; 315 } 316 317 318/******* WRITER *******/ 319 320 /** 321 * Serialize geometries into TWKB string. 322 * 323 * @return string The WKB string representation of the input geometries 324 * @param Geometry $geometry The geometry 325 * @param bool|true $writeAsHex Write the result in binary or hexadecimal system 326 * @param null $decimalDigitsXY Coordinate precision of X and Y. Default is 5 decimals 327 * @param null $decimalDigitsZ Coordinate precision of Z. Default is 0 decimal 328 * @param null $decimalDigitsM Coordinate precision of M. Default is 0 decimal 329 * @param bool $includeSizes Includes the size in bytes of the remainder of the geometry after the size attribute. Default is false 330 * @param bool $includeBoundingBoxes Includes the coordinates of bounding box' two corner. Default is false 331 * 332 * @return string binary or hexadecimal representation of TWKB 333 */ 334 public function write(Geometry $geometry, $writeAsHex = false, $decimalDigitsXY = null, $decimalDigitsZ = null, $decimalDigitsM = null, $includeSizes = false, $includeBoundingBoxes = false) 335 { 336 $this->writer = new BinaryWriter(); 337 338 $this->writeOptions = [ 339 'decimalDigitsXY' => $decimalDigitsXY !== null ? $decimalDigitsXY : $this->writeOptions['decimalDigitsXY'], 340 'decimalDigitsZ' => $decimalDigitsZ !== null ? $decimalDigitsZ : $this->writeOptions['decimalDigitsZ'], 341 'decimalDigitsM' => $decimalDigitsM !== null ? $decimalDigitsM : $this->writeOptions['decimalDigitsM'], 342 'includeSize' => $includeSizes ? true : $this->writeOptions['includeSize'], 343 'includeBoundingBoxes' => $includeBoundingBoxes ? true : $this->writeOptions['includeBoundingBoxes'] 344 ]; 345 $this->writeOptions = array_merge( 346 $this->writeOptions, 347 [ 348 'xyFactor' => pow(10, $this->writeOptions['decimalDigitsXY']), 349 'zFactor' => pow(10, $this->writeOptions['decimalDigitsZ']), 350 'mFactor' => pow(10, $this->writeOptions['decimalDigitsM']) 351 ] 352 ); 353 354 $twkb = $this->writeGeometry($geometry); 355 356 return $writeAsHex ? current(unpack('H*', $twkb)) : $twkb; 357 } 358 359 /** 360 * @param Geometry $geometry 361 * @return string 362 */ 363 protected function writeGeometry($geometry) 364 { 365 $this->writeOptions['hasZ'] = $geometry->hasZ(); 366 $this->writeOptions['hasM'] = $geometry->isMeasured(); 367 368 // Type and precision 369 $type = self::$typeMap[$geometry->geometryType()] + 370 (BinaryWriter::zigZagEncode($this->writeOptions['decimalDigitsXY']) << 4); 371 $twkbHead = $this->writer->writeUInt8($type); 372 373 // Is there extended precision information? 374 $metadataHeader = $this->writeOptions['includeBoundingBoxes'] << 0; 375 // Is there extended precision information? 376 $metadataHeader += $this->writeOptions['includeSize'] << 1; 377 // Is there an ID list? 378 // TODO: implement this (needs metadata support in geoPHP) 379 //$metadataHeader += $this->writeOptions['hasIdList'] << 2; 380 // Is there extended precision information? 381 $metadataHeader += ($geometry->hasZ() || $geometry->isMeasured()) << 3; 382 // Is this an empty geometry? 383 $metadataHeader += $geometry->isEmpty() << 4; 384 385 $twkbHead .= $this->writer->writeUInt8($metadataHeader); 386 387 $twkbGeom = ''; 388 if (!$geometry->isEmpty()) { 389 $this->lastPoint = new Point(0, 0, 0, 0); 390 391 switch ($geometry->geometryType()) { 392 case Geometry::POINT: 393 /** @var Point $geometry */ 394 $twkbGeom .= $this->writePoint($geometry); 395 break; 396 case Geometry::LINE_STRING: 397 /** @var LineString $geometry */ 398 $twkbGeom .= $this->writeLineString($geometry); 399 break; 400 case Geometry::POLYGON: 401 /** @var Polygon $geometry */ 402 $twkbGeom .= $this->writePolygon($geometry); 403 break; 404 case Geometry::MULTI_POINT: 405 case Geometry::MULTI_LINE_STRING: 406 case Geometry::MULTI_POLYGON: 407 case Geometry::GEOMETRY_COLLECTION: 408 /** @var Collection $geometry */ 409 $twkbGeom .= $this->writeMulti($geometry); 410 break; 411 } 412 } 413 414 if ($this->writeOptions['includeBoundingBoxes']) { 415 $bBox = $geometry->getBoundingBox(); 416 // X 417 $twkbBox = $this->writer->writeSVarInt($bBox['minx'] * $this->writeOptions['xyFactor']); 418 $twkbBox .= $this->writer->writeSVarInt(($bBox['maxx'] - $bBox['minx']) * $this->writeOptions['xyFactor']); 419 // Y 420 $twkbBox .= $this->writer->writeSVarInt($bBox['miny'] * $this->writeOptions['xyFactor']); 421 $twkbBox .= $this->writer->writeSVarInt(($bBox['maxy'] - $bBox['miny']) * $this->writeOptions['xyFactor']); 422 if ($geometry->hasZ()) { 423 $bBox['minz'] = $geometry->minimumZ(); 424 $bBox['maxz'] = $geometry->maximumZ(); 425 $twkbBox .= $this->writer->writeSVarInt(round($bBox['minz'] * $this->writeOptions['zFactor'])); 426 $twkbBox .= $this->writer->writeSVarInt(round(($bBox['maxz'] - $bBox['minz']) * $this->writeOptions['zFactor'])); 427 } 428 if ($geometry->isMeasured()) { 429 $bBox['minm'] = $geometry->minimumM(); 430 $bBox['maxm'] = $geometry->maximumM(); 431 $twkbBox .= $this->writer->writeSVarInt($bBox['minm'] * $this->writeOptions['mFactor']); 432 $twkbBox .= $this->writer->writeSVarInt(($bBox['maxm'] - $bBox['minm']) * $this->writeOptions['mFactor']); 433 } 434 $twkbGeom = $twkbBox . $twkbGeom; 435 } 436 437 if ($geometry->hasZ() || $geometry->isMeasured()) { 438 $extendedPrecision = 0; 439 if ($geometry->hasZ()) { 440 $extendedPrecision |= ($geometry->hasZ() ? 0x1 : 0) | ($this->writeOptions['decimalDigitsZ'] << 2); 441 } 442 if ($geometry->isMeasured()) { 443 $extendedPrecision |= ($geometry->isMeasured() ? 0x2 : 0) | ($this->writeOptions['decimalDigitsM'] << 5); 444 } 445 $twkbHead .= $this->writer->writeUInt8($extendedPrecision); 446 } 447 if ($this->writeOptions['includeSize']) { 448 $twkbHead .= $this->writer->writeUVarInt(strlen($twkbGeom)); 449 } 450 451 return $twkbHead . $twkbGeom; 452 } 453 454 /** 455 * @param Point $geometry 456 * @return string 457 */ 458 protected function writePoint($geometry) 459 { 460 $x = round($geometry->x() * $this->writeOptions['xyFactor']); 461 $y = round($geometry->y() * $this->writeOptions['xyFactor']); 462 $z = round($geometry->z() * $this->writeOptions['zFactor']); 463 $m = round($geometry->m() * $this->writeOptions['mFactor']); 464 465 $twkb = $this->writer->writeSVarInt($x - $this->lastPoint->x()); 466 $twkb .= $this->writer->writeSVarInt($y - $this->lastPoint->y()); 467 if ($this->writeOptions['hasZ']) { 468 $twkb .= $this->writer->writeSVarInt($z - $this->lastPoint->z()); 469 } 470 if ($this->writeOptions['hasM']) { 471 $twkb .= $this->writer->writeSVarInt($m - $this->lastPoint->m()); 472 } 473 474 $this->lastPoint = new Point($x, $y, $this->writeOptions['hasZ'] ? $z : null, $this->writeOptions['hasM'] ? $m : null); 475 476 return $twkb; 477 } 478 479 /** 480 * @param LineString $geometry 481 * @return string 482 */ 483 protected function writeLineString($geometry) 484 { 485 $twkb = $this->writer->writeUVarInt($geometry->numPoints()); 486 foreach ($geometry->getComponents() as $component) { 487 $twkb .= $this->writePoint($component); 488 } 489 return $twkb; 490 } 491 492 /** 493 * @param Polygon $geometry 494 * @return string 495 */ 496 protected function writePolygon($geometry) 497 { 498 $twkb = $this->writer->writeUVarInt($geometry->numGeometries()); 499 foreach ($geometry->getComponents() as $component) { 500 $twkb .= $this->writeLineString($component); 501 } 502 return $twkb; 503 } 504 505 /** 506 * @param Collection $geometry 507 * @return string 508 */ 509 protected function writeMulti($geometry) 510 { 511 $twkb = $this->writer->writeUVarInt($geometry->numGeometries()); 512 //if ($geometry->hasIdList()) { 513 // foreach ($geometry->getComponents() as $component) { 514 // $this->writer->writeUVarInt($component->getId()); 515 // } 516 //} 517 foreach ($geometry->getComponents() as $component) { 518 if ($geometry->geometryType() !== Geometry::GEOMETRY_COLLECTION) { 519 $func = 'write' . $component->geometryType(); 520 $twkb .= $this->$func($component); 521 } else { 522 $twkb .= $this->writeGeometry($component); 523 } 524 } 525 return $twkb; 526 } 527} 528