1<?php 2 3namespace geoPHP\Adapter; 4 5use geoPHP\Geometry\Geometry; 6use geoPHP\Geometry\GeometryCollection; 7use geoPHP\Geometry\Point; 8use geoPHP\Geometry\MultiPoint; 9use geoPHP\Geometry\LineString; 10use geoPHP\Geometry\MultiLineString; 11use geoPHP\Geometry\Polygon; 12use geoPHP\Geometry\MultiPolygon; 13 14/* 15 * (c) Patrick Hayes 16 * 17 * This code is open-source and licenced under the Modified BSD License. 18 * For the full copyright and license information, please view the LICENSE 19 * file that was distributed with this source code. 20 */ 21 22/** 23 * PHP Geometry/WKB encoder/decoder 24 * Reader can decode EWKB too. Writer always encodes valid WKBs 25 * 26 */ 27class WKB implements GeoAdapter 28{ 29 const Z_MASK = 0x80000000; 30 const M_MASK = 0x40000000; 31 const SRID_MASK = 0x20000000; 32 const WKB_XDR = 1; 33 const WKB_NDR = 0; 34 35 protected $hasZ = false; 36 37 protected $hasM = false; 38 39 protected $hasSRID = false; 40 41 protected $SRID = null; 42 43 protected $dimension = 2; 44 45 /** @var BinaryReader $reader */ 46 protected $reader; 47 48 /** @var BinaryWriter $writer */ 49 protected $writer; 50 51 /** @var array Maps Geometry types to WKB type codes */ 52 public static $typeMap = [ 53 Geometry::POINT => 1, 54 Geometry::LINE_STRING => 2, 55 Geometry::POLYGON => 3, 56 Geometry::MULTI_POINT => 4, 57 Geometry::MULTI_LINE_STRING => 5, 58 Geometry::MULTI_POLYGON => 6, 59 Geometry::GEOMETRY_COLLECTION => 7, 60 //Not supported types: 61 Geometry::CIRCULAR_STRING => 8, 62 Geometry::COMPOUND_CURVE => 9, 63 Geometry::CURVE_POLYGON => 10, 64 Geometry::MULTI_CURVE => 11, 65 Geometry::MULTI_SURFACE => 12, 66 Geometry::CURVE => 13, 67 Geometry::SURFACE => 14, 68 Geometry::POLYHEDRAL_SURFACE => 15, 69 Geometry::TIN => 16, 70 Geometry::TRIANGLE => 17, 71 ]; 72 73 /** 74 * Read WKB into geometry objects 75 * 76 * @param string $wkb Well-known-binary string 77 * @param bool $isHexString If this is a hexadecimal string that is in need of packing 78 * 79 * @return Geometry 80 * 81 * @throws \Exception 82 */ 83 public function read($wkb, $isHexString = false) 84 { 85 if ($isHexString) { 86 $wkb = pack('H*', $wkb); 87 } 88 89 if (empty($wkb)) { 90 throw new \Exception('Cannot read empty WKB geometry. Found ' . gettype($wkb)); 91 } 92 93 $this->reader = new BinaryReader($wkb); 94 95 $geometry = $this->getGeometry(); 96 97 $this->reader->close(); 98 99 return $geometry; 100 } 101 102 /** 103 * @return Geometry 104 * @throws \Exception 105 */ 106 protected function getGeometry() 107 { 108 $this->hasZ = false; 109 $this->hasM = false; 110 $SRID = null; 111 112 $this->reader->setEndianness( 113 $this->reader->readSInt8() === self::WKB_XDR ? BinaryReader::LITTLE_ENDIAN : BinaryReader::BIG_ENDIAN 114 ); 115 116 $wkbType = $this->reader->readUInt32(); 117 118 if (($wkbType & $this::SRID_MASK) === $this::SRID_MASK) { 119 $SRID = $this->reader->readUInt32(); 120 } 121 $geometryType = null; 122 if ($wkbType >= 1000 && $wkbType < 2000) { 123 $this->hasZ = true; 124 $geometryType = $wkbType - 1000; 125 } elseif ($wkbType >= 2000 && $wkbType < 3000) { 126 $this->hasM = true; 127 $geometryType = $wkbType - 2000; 128 } elseif ($wkbType >= 3000 && $wkbType < 4000) { 129 $this->hasZ = true; 130 $this->hasM = true; 131 $geometryType = $wkbType - 3000; 132 } 133 134 if ($wkbType & $this::Z_MASK) { 135 $this->hasZ = true; 136 } 137 if ($wkbType & $this::M_MASK) { 138 $this->hasM = true; 139 } 140 $this->dimension = 2 + ($this->hasZ ? 1 : 0) + ($this->hasM ? 1 : 0); 141 142 if (!$geometryType) { 143 $geometryType = $wkbType & 0xF; // remove any masks from type 144 } 145 $geometry = null; 146 switch ($geometryType) { 147 case 1: 148 $geometry = $this->getPoint(); 149 break; 150 case 2: 151 $geometry = $this->getLineString(); 152 break; 153 case 3: 154 $geometry = $this->getPolygon(); 155 break; 156 case 4: 157 $geometry = $this->getMulti('Point'); 158 break; 159 case 5: 160 $geometry = $this->getMulti('LineString'); 161 break; 162 case 6: 163 $geometry = $this->getMulti('Polygon'); 164 break; 165 case 7: 166 $geometry = $this->getMulti('Geometry'); 167 break; 168 default: 169 throw new \Exception( 170 'Geometry type ' . $geometryType . 171 ' (' . (array_search($geometryType, self::$typeMap) ?: 'unknown') . ') not supported' 172 ); 173 } 174 if ($geometry && $SRID) { 175 $geometry->setSRID($SRID); 176 } 177 return $geometry; 178 } 179 180 protected function getPoint() 181 { 182 $coordinates = $this->reader->readDoubles($this->dimension * 8); 183 $point = null; 184 switch (count($coordinates)) { 185 case 2: 186 $point = new Point($coordinates[0], $coordinates[1]); 187 break; 188 case 3: 189 if ($this->hasZ) { 190 $point = new Point($coordinates[0], $coordinates[1], $coordinates[2]); 191 } else { 192 $point = new Point($coordinates[0], $coordinates[1], null, $coordinates[2]); 193 } 194 break; 195 case 4: 196 $point = new Point($coordinates[0], $coordinates[1], $coordinates[2], $coordinates[3]); 197 break; 198 } 199 return $point; 200 } 201 202 protected function getLineString() 203 { 204 // Get the number of points expected in this string out of the first 4 bytes 205 $lineLength = $this->reader->readUInt32(); 206 207 // Return an empty linestring if there is no line-length 208 if (!$lineLength) { 209 return new LineString(); 210 } 211 212 $components = []; 213 for ($i = 0; $i < $lineLength; ++$i) { 214 $point = $this->getPoint(); 215 if ($point) { 216 $components[] = $point; 217 } 218 } 219 return new LineString($components); 220 } 221 222 protected function getPolygon() 223 { 224 // Get the number of linestring expected in this poly out of the first 4 bytes 225 $polyLength = $this->reader->readUInt32(); 226 227 $components = []; 228 $i = 1; 229 while ($i <= $polyLength) { 230 $ring = $this->getLineString(); 231 if (!$ring->isEmpty()) { 232 $components[] = $ring; 233 } 234 $i++; 235 } 236 237 return new Polygon($components); 238 } 239 240 protected function getMulti($type) 241 { 242 // Get the number of items expected in this multi out of the first 4 bytes 243 $multiLength = $this->reader->readUInt32(); 244 245 $components = []; 246 for ($i = 0; $i < $multiLength; $i++) { 247 $component = $this->getGeometry(); 248 $component->setSRID(null); 249 $components[] = $component; 250 } 251 switch ($type) { 252 case 'Point': 253 return new MultiPoint($components); 254 case 'LineString': 255 return new MultiLineString($components); 256 case 'Polygon': 257 return new MultiPolygon($components); 258 case 'Geometry': 259 return new GeometryCollection($components); 260 } 261 return null; 262 } 263 264 /** 265 * Serialize geometries into WKB string. 266 * 267 * @param Geometry $geometry The geometry 268 * @param boolean $writeAsHex Write the result in binary or hexadecimal system 269 * @param boolean $bigEndian Write in BigEndian or LittleEndian byte order 270 * 271 * @return string The WKB string representation of the input geometries 272 */ 273 public function write(Geometry $geometry, $writeAsHex = false, $bigEndian = false) 274 { 275 276 $this->writer = new BinaryWriter($bigEndian ? BinaryWriter::BIG_ENDIAN : BinaryWriter::LITTLE_ENDIAN); 277 278 $wkb = $this->writeGeometry($geometry); 279 280 return $writeAsHex ? current(unpack('H*', $wkb)) : $wkb; 281 } 282 283 /** 284 * @param Geometry $geometry 285 * @return string 286 */ 287 protected function writeGeometry($geometry) 288 { 289 $this->hasZ = $geometry->hasZ(); 290 $this->hasM = $geometry->isMeasured(); 291 292 $wkb = $this->writer->writeSInt8($this->writer->isBigEndian() ? self::WKB_NDR : self::WKB_XDR); 293 $wkb .= $this->writeType($geometry); 294 switch ($geometry->geometryType()) { 295 case Geometry::POINT: 296 /** @var Point $geometry */ 297 $wkb .= $this->writePoint($geometry); 298 break; 299 case Geometry::LINE_STRING: 300 /** @var LineString $geometry */ 301 $wkb .= $this->writeLineString($geometry); 302 break; 303 case Geometry::POLYGON: 304 /** @var Polygon $geometry */ 305 $wkb .= $this->writePolygon($geometry); 306 break; 307 case Geometry::MULTI_POINT: 308 /** @var MultiPoint $geometry */ 309 $wkb .= $this->writeMulti($geometry); 310 break; 311 case Geometry::MULTI_LINE_STRING: 312 /** @var MultiLineString $geometry */ 313 $wkb .= $this->writeMulti($geometry); 314 break; 315 case Geometry::MULTI_POLYGON: 316 /** @var MultiPolygon $geometry */ 317 $wkb .= $this->writeMulti($geometry); 318 break; 319 case Geometry::GEOMETRY_COLLECTION: 320 /** @var GeometryCollection $geometry */ 321 $wkb .= $this->writeMulti($geometry); 322 break; 323 } 324 return $wkb; 325 } 326 327 /** 328 * @param Point $point 329 * @return string 330 */ 331 protected function writePoint($point) 332 { 333 if ($point->isEmpty()) { 334 return $this->writer->writeDouble(NAN) . $this->writer->writeDouble(NAN); 335 } 336 $wkb = $this->writer->writeDouble($point->x()) . $this->writer->writeDouble($point->y()); 337 338 if ($this->hasZ) { 339 $wkb .= $this->writer->writeDouble($point->z()); 340 } 341 if ($this->hasM) { 342 $wkb .= $this->writer->writeDouble($point->m()); 343 } 344 return $wkb; 345 } 346 347 /** 348 * @param LineString $line 349 * @return string 350 */ 351 protected function writeLineString($line) 352 { 353 // Set the number of points in this line 354 $wkb = $this->writer->writeUInt32($line->numPoints()); 355 356 // Set the coords 357 foreach ($line->getComponents() as $i => $point) { 358 $wkb .= $this->writePoint($point); 359 } 360 361 return $wkb; 362 } 363 364 /** 365 * @param Polygon $poly 366 * @return string 367 */ 368 protected function writePolygon($poly) 369 { 370 // Set the number of lines in this poly 371 $wkb = $this->writer->writeUInt32($poly->numGeometries()); 372 373 // Write the lines 374 foreach ($poly->getComponents() as $line) { 375 $wkb .= $this->writeLineString($line); 376 } 377 378 return $wkb; 379 } 380 381 /** 382 * @param MultiPoint|MultiPolygon|MultiLineString|GeometryCollection $geometry 383 * @return string 384 */ 385 protected function writeMulti($geometry) 386 { 387 // Set the number of components 388 $wkb = $this->writer->writeUInt32($geometry->numGeometries()); 389 390 // Write the components 391 foreach ($geometry->getComponents() as $component) { 392 $wkb .= $this->writeGeometry($component); 393 } 394 395 return $wkb; 396 } 397 398 /** 399 * @param Geometry $geometry 400 * @param bool $writeSRID 401 * @return string 402 */ 403 protected function writeType($geometry, $writeSRID = false) 404 { 405 $type = self::$typeMap[$geometry->geometryType()]; 406 // Binary OR to mix in additional properties 407 if ($this->hasZ) { 408 $type = $type | $this::Z_MASK; 409 } 410 if ($this->hasM) { 411 $type = $type | $this::M_MASK; 412 } 413 if ($geometry->SRID() && $writeSRID) { 414 $type = $type | $this::SRID_MASK; 415 } 416 return $this->writer->writeUInt32($type) . 417 ($geometry->SRID() && $writeSRID ? $this->writer->writeUInt32($this->SRID) : ''); 418 } 419} 420