From 6efb3fe3168120d3681323a74e84752f5159c510 Mon Sep 17 00:00:00 2001 From: Jelle Sebreghts Date: Thu, 12 May 2016 12:33:16 +0200 Subject: [PATCH 1/4] Added an Esri JSON Adapter. --- geoPHP.inc | 6 + lib/adapters/EsriJSON.class.php | 518 ++++++++++++++++++++++++++++++++ 2 files changed, 524 insertions(+) create mode 100644 lib/adapters/EsriJSON.class.php diff --git a/geoPHP.inc b/geoPHP.inc index 78663042..159adfd4 100644 --- a/geoPHP.inc +++ b/geoPHP.inc @@ -10,6 +10,7 @@ // Adapters include_once("lib/adapters/GeoAdapter.class.php"); // Abtract class include_once("lib/adapters/GeoJSON.class.php"); +include_once("lib/adapters/EsriJSON.class.php"); include_once("lib/adapters/WKT.class.php"); include_once("lib/adapters/EWKT.class.php"); include_once("lib/adapters/WKB.class.php"); @@ -97,6 +98,8 @@ class geoPHP 'ewkb' => 'EWKB', 'json' => 'GeoJSON', 'geojson' => 'GeoJSON', + 'esrijson' => 'EsriJSON', + 'argcgis' => 'EsriJSON', 'kml' => 'KML', 'gpx' => 'GPX', 'georss' => 'GeoRSS', @@ -256,6 +259,9 @@ class geoPHP // Detect GeoJSON - first char starts with { if ($bytes[1] == 123) { + if (strpos($input, 'esri') !== FALSE) { + return 'esrijson'; + } return 'json'; } diff --git a/lib/adapters/EsriJSON.class.php b/lib/adapters/EsriJSON.class.php new file mode 100644 index 00000000..1afbbf5b --- /dev/null +++ b/lib/adapters/EsriJSON.class.php @@ -0,0 +1,518 @@ +geometry) ? $input->geometry->spatialReference : $input->spatialReference; + + if (isset($input->x) && is_numeric($input->x) && isset($input->y) && is_numeric($input->y)) { + $coords = array($input->x, $input->y); + if (isset($input->z)) { + $coords[] = $input->z; + } + return $this->arrayToPoint($coords); + } + + if (isset($input->points) && $input->points) { + return $this->arrayToMultiPoint($input->points); + } + + if (isset($input->paths) && $input->paths) { + if (count($input->paths) === 1) { + return $this->arrayToLineString($input->paths[0]); + } + else { + return $this->arrayToMultiLineString($input->paths); + } + } + + if ($input->rings) { + return $this->convertRingsToGeometry($input->rings); + } + + + if ($input->compressedGeometry || $input->geometry) { + if ($input->compressedGeometry) { + $input->geometry = (object) array( + 'paths' => array( + $this->decompressGeometry($input->compressedGeometry) + ), + ); + } + return $this->read($input->geometry); + } + // Should have returned something by now. + throw new Exception('Invalid JSON'); + } + + protected function arrayToPoint($array) { + return new Point( + isset($array[0]) ? $array[0] : NULL, isset($array[1]) ? $array[1] : NULL, isset($array[2]) ? $array[2] : NULL + ); + } + + protected function arrayToMultiPoint($array) { + $points = array(); + foreach ($array as $comp_array) { + $points[] = $this->arrayToPoint($comp_array); + } + return new MultiPoint($points); + } + + protected function arrayToLineString($array) { + $points = array(); + foreach ($array as $comp_array) { + $points[] = $this->arrayToPoint($comp_array); + } + return new LineString($points); + } + + protected function arrayToPolygon($array) { + $lines = array(); + foreach ($array as $comp_array) { + $lines[] = $this->arrayToLineString($comp_array); + } + return new Polygon($lines); + } + + protected function arrayToMultiLineString($array) { + $lines = array(); + foreach ($array as $comp_array) { + $lines[] = $this->arrayToLineString($comp_array); + } + return new MultiLineString($lines); + } + + protected function arrayToMultiPolygon($array) { + $polys = array(); + foreach ($array as $comp_array) { + $polys[] = $this->arrayToPolygon($comp_array); + } + return new MultiPolygon($polys); + } + + /** + * Serializes an object into a geojson string + * + * + * @param Geometry $obj The object to serialize + * + * @return string The GeoJSON string + */ + public function write(Geometry $geometry, $return_array = FALSE) { + if ($return_array) { + return $this->getArray($geometry); + } + else { + return json_encode($this->getArray($geometry)); + } + } + + protected function getArray(Geometry $geometry) { + $result = array('spatialReference' => (object) array('wkid' => 4326)); + switch ($geometry->geometryType()) { + case "Point": + $result['x'] = $geometry->x(); + $result['y'] = $geometry->y(); + if ($geometry->hasZ()) { + $result['z'] = $geometry->z(); + } + break; + case "MultiPoint": + $result['points'] = $geometry->asArray(); + break; + case "LineString": + $result['paths'] = array($geometry->asArray()); + break; + case "MultiLineString": + $result['paths'] = $geometry->asArray(); + break; + case "Polygon": + $result['rings'] = $this->orientRings($geometry->asArray()); + break; + case "MultiPolygon": + $result['rings'] = $this->flattenMultiPolygonRings($geometry->asArray()); + break; + case "GeometryCollection": + $result = array(); + foreach ($geometry->getComponents() as $component) { + $result[] = $this->getArray($component); + } + unset($result['spatialReference']); + break; + } + + return $result; + } + + /** + * Decompresses compressed geometry. + * + * + * @param string $str The compressed geometry. + * @return array The decompressed geometry points. + */ + protected function decompressGeometry($str) { + $xDiffPrev = 0; + $yDiffPrev = 0; + $points = array(); + $strings = array(); + // Split the string into an array on the + and - characters + $string = preg_match_all('/((\+|\-)[^\+\-]+)/', $str, $strings); + + // The first value is the coefficient in base 32 + $coefficient = intval($strings[0], 32); + + for ($i = 1; $i < count($strings); $i += 2) { + // $i is the offset for the $x value + // Convert the value from base 32 and add the previous $x value + $x = (intval($strings[$i], 32) + $xDiffPrev); + $xDiffPrev = $x; + + // j+1 is the offset for the y value + // Convert the value from base 32 and add the previous y value + $y = (intval($strings[i + 1], 32) + $yDiffPrev); + $yDiffPrev = $y; + array_push($points, array($x / $coefficient, $y / $coefficient)); + } + + return $points; + } + + /** + * Checks if the first and last points of a ring are equal and closes the + * ring if not. + * + * + * @param array $coordinates The coordinates of the ring. + * @return array The coordinates of the closed ring. + */ + protected function closeRing($coordinates) { + if (!$this->pointsEqual($coordinates[0], $coordinates[count($coordinates) - 1])) { + array_push($coordinates, $coordinates[0]); + } + return $coordinates; + } + + /** + * Checks if 2 x,y points are equal. + * + * + * @param array $a The coordinates of point a. + * @param array $b The coordinates of point b. + * @return boolean Whether or not the points are equal. + */ + protected function pointsEqual($a, $b) { + for ($i = 0; $i < count($a); $i++) { + if ($a[$i] !== $b[$i]) { + return FALSE; + } + } + return TRUE; + } + + /** + * Determine if polygon ring coordinates are clockwise. Clockwise signifies + * outer ring, counter-clockwise an inner ring or hole. This logic was found + * at http://stackoverflow.com/questions/1165647/how-to-determine-if-a-list- + * of-polygon-points-are-in-clockwise-order + * + * + * @param array $ringToTest The coordinates of the ring to test. + * @return boolean Wether or not the ring is clockwise. + */ + protected function ringIsClockwise($ringToTest) { + $total = 0; + $rLength = count($ringToTest); + $pt1 = $ringToTest[0]; + for ($i = 0; $i < $rLength - 1; $i++) { + $pt2 = $ringToTest[$i + 1]; + $total += ($pt2[0] - $pt1[0]) * ($pt2[1] + $pt1[1]); + $pt1 = $pt2; + } + return ($total >= 0); + } + + /** + * Ensures that rings are oriented in the right directions outer rings are + * clockwise, holes are counterclockwise. + * + * + * @param array $polygon The rings to orient. + * @return array The oriented rings. + */ + protected function orientRings($polygon) { + $output = array(); + $outerRing = $this->closeRing(array_shift($polygon)); + if (count($outerRing) >= 4) { + if (!$this->ringIsClockwise($outerRing)) { + $outerRing = array_reverse($outerRing); + } + + array_push($output, $outerRing); + + for ($i = 0; $i < count($polygon); $i++) { + $hole = $this->closeRing($polygon[$i]); + if (count($hole) >= 4) { + if ($this->ringIsClockwise($hole)) { + $hole = array_reverse($hole); + } + array_push($output, $hole); + } + } + } + return $output; + } + + /** This function flattens holes in multipolygons to one array of polygons. + * [ + * [ + * [ array of outer coordinates ] + * [ hole coordinates ] + * [ hole coordinates ] + * ], + * [ + * [ array of outer coordinates ] + * [ hole coordinates ] + * [ hole coordinates ] + * ], + * ] + * becomes + * [ + * [ array of outer coordinates ] + * [ hole coordinates ] + * [ hole coordinates ] + * [ array of outer coordinates ] + * [ hole coordinates ] + * [ hole coordinates ] + * ] + * + * + * @param array $rings The rings to flatten. + * @return array The flattened rings. + */ + protected function flattenMultiPolygonRings($rings) { + $output = array(); + for ($i = 0; $i < count($rings); $i++) { + $polygon = $this->orientRings($rings[$i]); + for ($j = count($polygon) - 1; $j >= 0; $j--) { + array_push($output, $polygon[$j]); + } + } + return $output; + } + + /** + * Checks if two edges intersect. + * + * + * @param array $a1 First coordinate of the first edge to check. + * @param array $a2 Second coordinate of the first edge to check. + * @param array $b1 First coordinate of the second edge to check. + * @param array $b2 Second coordinate of the second edge to check. + * @return boolean Whether or not the edges intersect. + */ + protected function edgeIntersectsEdge($a1, $a2, $b1, $b2) { + $$ua_t = ($b2[0] - $b1[0]) * ($a1[1] - $b1[1]) - ($b2[1] - $b1[1]) * ($a1[0] - $b1[0]); + $$ub_t = ($a2[0] - $a1[0]) * ($a1[1] - $b1[1]) - ($a2[1] - $a1[1]) * ($a1[0] - $b1[0]); + $$u_b = ($b2[1] - $b1[1]) * ($a2[0] - $a1[0]) - ($b2[0] - $b1[0]) * ($a2[1] - $a1[1]); + + if ($u_b !== 0) { + $ua = $ua_t / $u_b; + $ub = $ub_t / $u_b; + + if (0 <= $ua && $ua <= 1 && 0 <= $ub && $ub <= 1) { + return TRUE; + } + } + + return FALSE; + } + + /** + * Checks if an array of coordinates intersect. + * + * @param array $a The first array of coordinates to check. + * @param array $b The second array of coordinates to check. + * @return boolean Whether or not the arrays of coordinates intersect. + */ + protected function arraysIntersectArrays($a, $b) { + if (is_numeric($a[0][0])) { + if (is_numeric($b[0][0])) { + for ($i = 0; $i < count($a) - 1; $i++) { + for ($j = 0; $j < count($b) - 1; $j++) { + if ($this->edgeIntersectsEdge($a[$i], $a[$i + 1], $b[$j], $b[$j + 1])) { + return TRUE; + } + } + } + } + else { + for ($k = 0; $k < count($b); $k++) { + if ($this->arraysIntersectArrays($a, $b[$k])) { + return TRUE; + } + } + } + } + else { + for ($l = 0; $l < count($a); $l++) { + if ($this->arraysIntersectArrays($a[$l], $b)) { + return TRUE; + } + } + } + return FALSE; + } + + /** + * Check if an array of coordinates contain a point. + * + * + * @param array $coordinates The array of coordinates to check. + * @param array $point The coordinates of the point to check. + * @return boolean Whether or not the array of coordinates contain the point. + */ + protected function coordinatesContainPoint($coordinates, $point) { + $contains = FALSE; + for ($i = -1, $l = count($coordinates), $j = $l - 1; ++$i < $l; $j = $i) { + if ((($coordinates[$i][1] <= $point[1] && $point[1] < $coordinates[$j][1]) || + ($coordinates[$j][1] <= $point[1] && $point[1] < $coordinates[$i][1])) && + ($point[0] < ($coordinates[j][0] - $coordinates[$i][0]) * ($point[1] - $coordinates[$i][1]) / ($coordinates[$j][1] - $coordinates[$i][1]) + $coordinates[$i][0])) { + $contains = !$contains; + } + } + return $contains; + } + + /** + * Checks if an array of coordinates contains an other array of coordinates. + * + * + * @param array $outer The array of outer coordinates. + * @param array $inner The array of inner coordinates. + * @return boolean Whether or not the outer array contains the inner array. + */ + protected function coordinatesContainCoordinates($outer, $inner) { + $intersects = $this->arraysIntersectArrays($outer, $inner); + $contains = $this->coordinatesContainPoint($outer, $inner[0]); + if (!$intersects && $contains) { + return TRUE; + } + return FALSE; + } + + /** + * Checks if any polygons in this array contain any other polygons in this + * array. Used for checking holes in arcgis rings. + * + * + * @param array $rings The argcis rings to check. + * @return Geometry The converted rings. + */ + protected function convertRingsToGeometry($rings) { + $outerRings = array(); + $holes = array(); + + // For each ring. + for ($r = 0; $r < count($rings); $r++) { + $ring = $this->closeRing($rings[$r]); + if (count($ring) < 4) { + continue; + } + // Is this ring an outer ring? Is it clockwise? + if ($this->ringIsClockwise($ring)) { + $polygon = array($ring); + // Push to outer rings. + array_push($outerRings, $polygon); + } + else { + // Counterclockwise push to holes. + array_push($holes, $ring); + } + } + + $uncontainedHoles = array(); + + // While there are holes left... + while (count($holes)) { + // Pop a hole off out stack. + $hole = array_pop($holes); + + // Loop over all outer rings and see if they contain our hole. + $contained = FALSE; + for ($x = count($outerRings) - 1; $x >= 0; $x--) { + $outerRing = $outerRings[$x][0]; + if ($this->coordinatesContainCoordinates($outerRing, $hole)) { + // The hole is contained push it into our polygon. + array_push($outerRings[$x], $hole); + $contained = TRUE; + break; + } + } + + // The ring is not contained in any outer ring. + // Sometimes this happens https://github.com/Esri/esri-leaflet/issues/320 + if (!$contained) { + array_push($uncontainedHoles, $hole); + } + } + + // If we couldn't match any holes using contains we can now try intersects... + while ($uncontainedHoles . length) { + // Pop a hole off out stack. + $hole = array_pop($uncontainedHoles); + + // Loop over all outer rings and see if any intersect our hole. + $intersects = FALSE; + for ($x = count($outerRings) - 1; $x >= 0; $x--) { + $outerRing = $outerRings[$x][0]; + if ($this->arraysIntersectArrays($outerRing, $hole)) { + // The hole intersects the outer ring push it into our polygon. + array_push($outerRings[$x], $hole); + $intersects = TRUE; + break; + } + } + + // Hole does not intersect ANY outer ring at this point. + // Make it an outer ring. + if (!$intersects) { + array_push($outerRings, array(array_reverse(hole))); + } + } + + if (count($outerRings) === 1) { + return $this->arrayToPolygon($outerRings[0]); + } + else { + return $this->arrayToMultiPolygon($outerRings); + } + } +} From 489f1e80f7c4d64bcd4ba0d9b07afeca022ed0ec Mon Sep 17 00:00:00 2001 From: Jelle Sebreghts Date: Thu, 12 May 2016 13:00:08 +0200 Subject: [PATCH 2/4] Added an Esri JSON Adapter. --- geoPHP.inc | 2 +- lib/adapters/EsriJSON.class.php | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/geoPHP.inc b/geoPHP.inc index 159adfd4..d0e06290 100644 --- a/geoPHP.inc +++ b/geoPHP.inc @@ -99,7 +99,7 @@ class geoPHP 'json' => 'GeoJSON', 'geojson' => 'GeoJSON', 'esrijson' => 'EsriJSON', - 'argcgis' => 'EsriJSON', + 'arcgis' => 'EsriJSON', 'kml' => 'KML', 'gpx' => 'GPX', 'georss' => 'GeoRSS', diff --git a/lib/adapters/EsriJSON.class.php b/lib/adapters/EsriJSON.class.php index 1afbbf5b..f6705e4e 100644 --- a/lib/adapters/EsriJSON.class.php +++ b/lib/adapters/EsriJSON.class.php @@ -28,8 +28,8 @@ public function read($input) { throw new Exception('Invalid JSON'); } - // TODO: conversion? - // $inputSpatialReference = isset($input->geometry) ? $input->geometry->spatialReference : $input->spatialReference; + // TODO: What if the wkid is different from 4326? + //$inputSpatialReference = isset($input->geometry) ? $input->geometry->spatialReference : $input->spatialReference; if (isset($input->x) && is_numeric($input->x) && isset($input->y) && is_numeric($input->y)) { $coords = array($input->x, $input->y); @@ -56,8 +56,7 @@ public function read($input) { return $this->convertRingsToGeometry($input->rings); } - - if ($input->compressedGeometry || $input->geometry) { + if ((isset($input->compressedGeometry) && $input->compressedGeometry) || (isset($input->geometry)) && $input->geometry) { if ($input->compressedGeometry) { $input->geometry = (object) array( 'paths' => array( @@ -67,6 +66,13 @@ public function read($input) { } return $this->read($input->geometry); } + if ((isset($input->features)) && $input->features) { + $geometries = array(); + foreach ($input->features as $feature) { + $geometries[] = $this->read($feature); + } + return new GeometryCollection($geometries); + } // Should have returned something by now. throw new Exception('Invalid JSON'); } From 0aa75e0d5a7a5aaf0ae5ccc067546d51fefe37e3 Mon Sep 17 00:00:00 2001 From: Jelle Sebreghts Date: Thu, 12 May 2016 14:07:57 +0200 Subject: [PATCH 3/4] Fix tests. --- lib/adapters/EsriJSON.class.php | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/adapters/EsriJSON.class.php b/lib/adapters/EsriJSON.class.php index f6705e4e..a98514cc 100644 --- a/lib/adapters/EsriJSON.class.php +++ b/lib/adapters/EsriJSON.class.php @@ -31,9 +31,9 @@ public function read($input) { // TODO: What if the wkid is different from 4326? //$inputSpatialReference = isset($input->geometry) ? $input->geometry->spatialReference : $input->spatialReference; - if (isset($input->x) && is_numeric($input->x) && isset($input->y) && is_numeric($input->y)) { + if (property_exists($input, 'x') && property_exists($input, 'y')) { $coords = array($input->x, $input->y); - if (isset($input->z)) { + if (property_exists($input, 'z')) { $coords[] = $input->z; } return $this->arrayToPoint($coords); @@ -52,7 +52,7 @@ public function read($input) { } } - if ($input->rings) { + if (property_exists($input, 'rings')) { return $this->convertRingsToGeometry($input->rings); } @@ -79,7 +79,9 @@ public function read($input) { protected function arrayToPoint($array) { return new Point( - isset($array[0]) ? $array[0] : NULL, isset($array[1]) ? $array[1] : NULL, isset($array[2]) ? $array[2] : NULL + isset($array[0]) ? $array[0] : NULL, + isset($array[1]) ? $array[1] : NULL, + isset($array[2]) ? $array[2] : NULL ); } @@ -166,11 +168,10 @@ protected function getArray(Geometry $geometry) { $result['rings'] = $this->flattenMultiPolygonRings($geometry->asArray()); break; case "GeometryCollection": - $result = array(); + $result['features'] = array(); foreach ($geometry->getComponents() as $component) { - $result[] = $this->getArray($component); + $result['features'][] = $this->getArray($component); } - unset($result['spatialReference']); break; } @@ -345,9 +346,9 @@ protected function flattenMultiPolygonRings($rings) { * @return boolean Whether or not the edges intersect. */ protected function edgeIntersectsEdge($a1, $a2, $b1, $b2) { - $$ua_t = ($b2[0] - $b1[0]) * ($a1[1] - $b1[1]) - ($b2[1] - $b1[1]) * ($a1[0] - $b1[0]); - $$ub_t = ($a2[0] - $a1[0]) * ($a1[1] - $b1[1]) - ($a2[1] - $a1[1]) * ($a1[0] - $b1[0]); - $$u_b = ($b2[1] - $b1[1]) * ($a2[0] - $a1[0]) - ($b2[0] - $b1[0]) * ($a2[1] - $a1[1]); + $ua_t = ($b2[0] - $b1[0]) * ($a1[1] - $b1[1]) - ($b2[1] - $b1[1]) * ($a1[0] - $b1[0]); + $ub_t = ($a2[0] - $a1[0]) * ($a1[1] - $b1[1]) - ($a2[1] - $a1[1]) * ($a1[0] - $b1[0]); + $u_b = ($b2[1] - $b1[1]) * ($a2[0] - $a1[0]) - ($b2[0] - $b1[0]) * ($a2[1] - $a1[1]); if ($u_b !== 0) { $ua = $ua_t / $u_b; @@ -410,7 +411,7 @@ protected function coordinatesContainPoint($coordinates, $point) { for ($i = -1, $l = count($coordinates), $j = $l - 1; ++$i < $l; $j = $i) { if ((($coordinates[$i][1] <= $point[1] && $point[1] < $coordinates[$j][1]) || ($coordinates[$j][1] <= $point[1] && $point[1] < $coordinates[$i][1])) && - ($point[0] < ($coordinates[j][0] - $coordinates[$i][0]) * ($point[1] - $coordinates[$i][1]) / ($coordinates[$j][1] - $coordinates[$i][1]) + $coordinates[$i][0])) { + ($point[0] < ($coordinates[$j][0] - $coordinates[$i][0]) * ($point[1] - $coordinates[$i][1]) / ($coordinates[$j][1] - $coordinates[$i][1]) + $coordinates[$i][0])) { $contains = !$contains; } } @@ -491,7 +492,7 @@ protected function convertRingsToGeometry($rings) { } // If we couldn't match any holes using contains we can now try intersects... - while ($uncontainedHoles . length) { + while (count($uncontainedHoles)) { // Pop a hole off out stack. $hole = array_pop($uncontainedHoles); From 6e5f60ea6861bd8dfeed5b060d925295ddb8617c Mon Sep 17 00:00:00 2001 From: Valdas Kalvaitis Date: Fri, 7 Oct 2016 13:00:54 +0300 Subject: [PATCH 4/4] use safe compare --- lib/adapters/EsriJSON.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/adapters/EsriJSON.class.php b/lib/adapters/EsriJSON.class.php index a98514cc..d25fe8b7 100644 --- a/lib/adapters/EsriJSON.class.php +++ b/lib/adapters/EsriJSON.class.php @@ -350,7 +350,7 @@ protected function edgeIntersectsEdge($a1, $a2, $b1, $b2) { $ub_t = ($a2[0] - $a1[0]) * ($a1[1] - $b1[1]) - ($a2[1] - $a1[1]) * ($a1[0] - $b1[0]); $u_b = ($b2[1] - $b1[1]) * ($a2[0] - $a1[0]) - ($b2[0] - $b1[0]) * ($a2[1] - $a1[1]); - if ($u_b !== 0) { + if ($u_b != 0) { $ua = $ua_t / $u_b; $ub = $ub_t / $u_b;