diff --git a/geoPHP.inc b/geoPHP.inc index 78663042..d0e06290 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', + 'arcgis' => '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..d25fe8b7 --- /dev/null +++ b/lib/adapters/EsriJSON.class.php @@ -0,0 +1,525 @@ +geometry) ? $input->geometry->spatialReference : $input->spatialReference; + + if (property_exists($input, 'x') && property_exists($input, 'y')) { + $coords = array($input->x, $input->y); + if (property_exists($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 (property_exists($input, 'rings')) { + return $this->convertRingsToGeometry($input->rings); + } + + if ((isset($input->compressedGeometry) && $input->compressedGeometry) || (isset($input->geometry)) && $input->geometry) { + if ($input->compressedGeometry) { + $input->geometry = (object) array( + 'paths' => array( + $this->decompressGeometry($input->compressedGeometry) + ), + ); + } + 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'); + } + + 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['features'] = array(); + foreach ($geometry->getComponents() as $component) { + $result['features'][] = $this->getArray($component); + } + 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 (count($uncontainedHoles)) { + // 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); + } + } +}