1<?php 2 3namespace geoPHP\Adapter; 4 5use geoPHP\Geometry\Collection; 6use geoPHP\geoPHP; 7use geoPHP\Geometry\Geometry; 8use geoPHP\Geometry\GeometryCollection; 9use geoPHP\Geometry\Point; 10use geoPHP\Geometry\MultiPoint; 11use geoPHP\Geometry\LineString; 12use geoPHP\Geometry\MultiLineString; 13use geoPHP\Geometry\Polygon; 14use geoPHP\Geometry\MultiPolygon; 15 16/** 17 * WKT (Well Known Text) Adapter 18 */ 19class WKT implements GeoAdapter 20{ 21 22 protected $hasZ = false; 23 24 protected $measured = false; 25 26 /** 27 * Determines if the given typeString is a valid WKT geometry type 28 * 29 * @param string $typeString Type to find, eg. "Point", or "LineStringZ" 30 * @return string|bool The geometry type if found or false 31 */ 32 public static function isWktType($typeString) 33 { 34 foreach (geoPHP::getGeometryList() as $geom => $type) { 35 if (strtolower((substr($typeString, 0, strlen($geom)))) == $geom) { 36 return $type; 37 } 38 } 39 return false; 40 } 41 42 /** 43 * Read WKT string into geometry objects 44 * 45 * @param string $wkt A WKT string 46 * @return Geometry 47 * @throws \Exception 48 */ 49 public function read($wkt) 50 { 51 $this->hasZ = false; 52 $this->measured = false; 53 54 $wkt = trim(strtoupper($wkt)); 55 $srid = null; 56 // If it contains a ';', then it contains additional SRID data 57 if (preg_match('/^SRID=(\d+);/', $wkt, $m)) { 58 $srid = $m[1]; 59 $wkt = substr($wkt, strlen($m[0])); 60 } 61 62 // If geos is installed, then we take a shortcut and let it parse the WKT 63 if (geoPHP::geosInstalled()) { 64 /** @noinspection PhpUndefinedClassInspection */ 65 $reader = new \GEOSWKTReader(); 66 try { 67 $geom = geoPHP::geosToGeometry($reader->read($wkt)); 68 if ($srid) { 69 $geom->setSRID($srid); 70 } 71 return $geom; 72 } catch (\Exception $e) { 73// if ($e->getMessage() !== 'IllegalArgumentException: Empty Points cannot be represented in WKB') { 74// throw $e; 75// } // else try with GeoPHP' parser 76 } 77 } 78 79 if ($geometry = $this->parseTypeAndGetData($wkt)) { 80 if ($geometry && $srid) { 81 $geometry->setSRID($srid); 82 } 83 return $geometry; 84 } 85 throw new \Exception('Invalid Wkt'); 86 } 87 88 /** 89 * @param string $wkt 90 * 91 * @return Geometry|null 92 * @throws \Exception 93 */ 94 private function parseTypeAndGetData($wkt) 95 { 96 // geometry type is the first word 97 if (preg_match('/^(?<type>[A-Z]+)\s*(?<z>Z*)(?<m>M*)\s*(?:\((?<data>.+)\)|(?<data_empty>EMPTY))$/', $wkt, $m)) { 98 $geometryType = $this->isWktType($m['type']); 99 // Not used yet 100 //$this->hasZ = $this->hasZ || $m['z']; 101 //$this->measured = $this->measured || $m['m']; 102 $dataString = $m['data'] ?: $m['data_empty']; 103 104 if ($geometryType) { 105 $method = 'parse' . $geometryType; 106 return call_user_func([$this, $method], $dataString); 107 } 108 throw new \Exception('Invalid WKT type "' . $m[1] . '"'); 109 } 110 throw new \Exception('Cannot parse WKT'); 111 } 112 113 private function parsePoint($dataString) 114 { 115 $dataString = trim($dataString); 116 // If it's marked as empty, then return an empty point 117 if ($dataString == 'EMPTY') { 118 return new Point(); 119 } 120 $z = $m = null; 121 $parts = explode(' ', $dataString); 122 if (isset($parts[2])) { 123 if ($this->measured) { 124 $m = $parts[2]; 125 } else { 126 $z = $parts[2]; 127 } 128 } 129 if (isset($parts[3])) { 130 $m = $parts[3]; 131 } 132 return new Point($parts[0], $parts[1], $z, $m); 133 } 134 135 private function parseLineString($dataString) 136 { 137 // If it's marked as empty, then return an empty line 138 if ($dataString == 'EMPTY') { 139 return new LineString(); 140 } 141 142 $points = []; 143 foreach (explode(',', $dataString) as $part) { 144 $points[] = $this->parsePoint($part); 145 } 146 return new LineString($points); 147 } 148 149 private function parsePolygon($dataString) 150 { 151 // If it's marked as empty, then return an empty polygon 152 if ($dataString == 'EMPTY') { 153 return new Polygon(); 154 } 155 156 $lines = []; 157 if (preg_match_all('/\(([^)(]*)\)/', $dataString, $m)) { 158 foreach ($m[1] as $part) { 159 $lines[] = $this->parseLineString($part); 160 } 161 } 162 return new Polygon($lines); 163 } 164 165 /** @noinspection PhpUnusedPrivateMethodInspection 166 * @param string $dataString 167 * 168 * @return MultiPoint 169 */ 170 private function parseMultiPoint($dataString) 171 { 172 // If it's marked as empty, then return an empty MultiPoint 173 if ($dataString == 'EMPTY') { 174 return new MultiPoint(); 175 } 176 177 $points = []; 178 /* Should understand both forms: 179 * MULTIPOINT ((1 2), (3 4)) 180 * MULTIPOINT (1 2, 3 4) 181 */ 182 foreach (explode(',', $dataString) as $part) { 183 $points[] = $this->parsePoint(trim($part, ' ()')); 184 } 185 return new MultiPoint($points); 186 } 187 188 /** @noinspection PhpUnusedPrivateMethodInspection 189 * @param string $dataString 190 * 191 * @return MultiLineString 192 */ 193 private function parseMultiLineString($dataString) 194 { 195 // If it's marked as empty, then return an empty multi-linestring 196 if ($dataString == 'EMPTY') { 197 return new MultiLineString(); 198 } 199 $lines = []; 200 if (preg_match_all('/(\([^(]+\)|EMPTY)/', $dataString, $m)) { 201 foreach ($m[1] as $part) { 202 $lines[] = $this->parseLineString(trim($part, ' ()')); 203 } 204 } 205 return new MultiLineString($lines); 206 } 207 208 /** @noinspection PhpUnusedPrivateMethodInspection 209 * @param string $dataString 210 * 211 * @return MultiPolygon 212 */ 213 private function parseMultiPolygon($dataString) 214 { 215 // If it's marked as empty, then return an empty multi-polygon 216 if ($dataString == 'EMPTY') { 217 return new MultiPolygon(); 218 } 219 220 $polygons = []; 221 if (preg_match_all('/(\(\([^(].+\)\)|EMPTY)/', $dataString, $m)) { 222 foreach ($m[0] as $part) { 223 $polygons[] = $this->parsePolygon($part); 224 } 225 } 226 return new MultiPolygon($polygons); 227 } 228 229 /** @noinspection PhpUnusedPrivateMethodInspection 230 * @param string $dataString 231 * 232 * @return GeometryCollection 233 */ 234 private function parseGeometryCollection($dataString) 235 { 236 // If it's marked as empty, then return an empty geom-collection 237 if ($dataString == 'EMPTY') { 238 return new GeometryCollection(); 239 } 240 241 $geometries = []; 242 while (strlen($dataString) > 0) { 243 // Matches the first balanced parenthesis group (or term EMPTY) 244 preg_match( 245 '/\((?>[^()]+|(?R))*\)|EMPTY/', 246 $dataString, 247 $m, 248 PREG_OFFSET_CAPTURE 249 ); 250 if (!isset($m[0])) { 251 // something weird happened, we stop here before running in an infinite loop 252 break; 253 } 254 $cutPosition = strlen($m[0][0]) + $m[0][1]; 255 $geometry = $this->parseTypeAndGetData(trim(substr($dataString, 0, $cutPosition))); 256 $geometries[] = $geometry; 257 $dataString = trim(substr($dataString, $cutPosition + 1)); 258 } 259 260 return new GeometryCollection($geometries); 261 } 262 263 264 /** 265 * Serialize geometries into a WKT string. 266 * 267 * @param Geometry $geometry 268 * 269 * @return string The WKT string representation of the input geometries 270 */ 271 public function write(Geometry $geometry) 272 { 273 // If geos is installed, then we take a shortcut and let it write the WKT 274 if (geoPHP::geosInstalled()) { 275 /** @noinspection PhpUndefinedClassInspection */ 276 $writer = new \GEOSWKTWriter(); 277 /** @noinspection PhpUndefinedMethodInspection */ 278 $writer->setRoundingPrecision(14); 279 /** @noinspection PhpUndefinedMethodInspection */ 280 $writer->setTrim(true); 281 /** @noinspection PhpUndefinedMethodInspection */ 282 return $writer->write($geometry->getGeos()); 283 } 284 $this->measured = $geometry->isMeasured(); 285 $this->hasZ = $geometry->hasZ(); 286 287 if ($geometry->isEmpty()) { 288 return strtoupper($geometry->geometryType()) . ' EMPTY'; 289 } 290 291 if ($data = $this->extractData($geometry)) { 292 $extension = ''; 293 if ($this->hasZ) { 294 $extension .= 'Z'; 295 } 296 if ($this->measured) { 297 $extension .= 'M'; 298 } 299 return strtoupper($geometry->geometryType()) . ($extension ? ' ' . $extension : '') . ' (' . $data . ')'; 300 } 301 return ''; 302 } 303 304 /** 305 * Extract geometry to a WKT string 306 * 307 * @param Geometry|Collection $geometry A Geometry object 308 * 309 * @return string 310 */ 311 public function extractData($geometry) 312 { 313 $parts = []; 314 switch ($geometry->geometryType()) { 315 case Geometry::POINT: 316 $p = $geometry->x() . ' ' . $geometry->y(); 317 if ($geometry->hasZ()) { 318 $p .= ' ' . $geometry->getZ(); 319 $this->hasZ = $this->hasZ || $geometry->hasZ(); 320 } 321 if ($geometry->isMeasured()) { 322 $p .= ' ' . $geometry->getM(); 323 $this->measured = $this->measured || $geometry->isMeasured(); 324 } 325 return $p; 326 case Geometry::LINE_STRING: 327 foreach ($geometry->getComponents() as $component) { 328 $parts[] = $this->extractData($component); 329 } 330 return implode(', ', $parts); 331 case Geometry::POLYGON: 332 case Geometry::MULTI_POINT: 333 case Geometry::MULTI_LINE_STRING: 334 case Geometry::MULTI_POLYGON: 335 foreach ($geometry->getComponents() as $component) { 336 if ($component->isEmpty()) { 337 $parts[] = 'EMPTY'; 338 } else { 339 $parts[] = '(' . $this->extractData($component) . ')'; 340 } 341 } 342 return implode(', ', $parts); 343 case Geometry::GEOMETRY_COLLECTION: 344 foreach ($geometry->getComponents() as $component) { 345 $this->hasZ = $this->hasZ || $geometry->hasZ(); 346 $this->measured = $this->measured || $geometry->isMeasured(); 347 348 $extension = ''; 349 if ($this->hasZ) { 350 $extension .= 'Z'; 351 } 352 if ($this->measured) { 353 $extension .= 'M'; 354 } 355 $data = $this->extractData($component); 356 $parts[] = strtoupper($component->geometryType()) 357 . ($extension ? ' ' . $extension : '') 358 . ($data ? ' (' . $data . ')' : ' EMPTY'); 359 } 360 return implode(', ', $parts); 361 } 362 return ''; 363 } 364} 365