$lat ) !== ( $yj > $lat ) ) { $x_cross = ( $xj - $xi ) * ( $lat - $yi ) / ( $yj - $yi ) + $xi; if ( $lon < $x_cross ) { $inside = ! $inside; } } } return $inside; } /** * Point in polygon: inside the outer ring and outside every hole. * * @param float $lon Point longitude. * @param float $lat Point latitude. * @param array $rings Polygon coordinates: [outer, hole1, hole2, ...]. * @return bool */ public static function point_in_polygon( $lon, $lat, array $rings ) { if ( empty( $rings ) || ! self::point_in_ring( $lon, $lat, $rings[0] ) ) { return false; } for ( $h = 1, $c = count( $rings ); $h < $c; $h++ ) { // A point strictly inside a hole is outside. A point on a hole's edge stays inside (generous). if ( self::point_in_ring( $lon, $lat, $rings[ $h ] ) && ! self::on_ring_edge( $lon, $lat, $rings[ $h ] ) ) { return false; } } return true; } /** * Point in a GeoJSON Polygon, MultiPolygon, Feature or FeatureCollection. * * @param float $lon Point longitude. * @param float $lat Point latitude. * @param array $geo Decoded GeoJSON. * @return bool */ public static function point_in_geojson( $lon, $lat, array $geo ) { $type = isset( $geo['type'] ) ? $geo['type'] : ''; switch ( $type ) { case 'Polygon': return isset( $geo['coordinates'] ) && self::point_in_polygon( $lon, $lat, $geo['coordinates'] ); case 'MultiPolygon': if ( empty( $geo['coordinates'] ) ) { return false; } foreach ( $geo['coordinates'] as $polygon ) { if ( self::point_in_polygon( $lon, $lat, $polygon ) ) { return true; } } return false; case 'Feature': return isset( $geo['geometry'] ) && is_array( $geo['geometry'] ) && self::point_in_geojson( $lon, $lat, $geo['geometry'] ); case 'FeatureCollection': foreach ( isset( $geo['features'] ) ? $geo['features'] : array() as $feature ) { if ( is_array( $feature ) && self::point_in_geojson( $lon, $lat, $feature ) ) { return true; } } return false; } return false; } /** * Is this a usable Polygon or MultiPolygon (numeric positions, closed rings of at least 4 positions)? * * @param mixed $geo Decoded GeoJSON geometry. * @return bool */ public static function is_valid_area( $geo ) { if ( ! is_array( $geo ) || ! isset( $geo['type'], $geo['coordinates'] ) || ! is_array( $geo['coordinates'] ) ) { return false; } if ( 'Polygon' === $geo['type'] ) { return self::valid_polygon( $geo['coordinates'] ); } if ( 'MultiPolygon' === $geo['type'] && ! empty( $geo['coordinates'] ) ) { foreach ( $geo['coordinates'] as $polygon ) { if ( ! is_array( $polygon ) || ! self::valid_polygon( $polygon ) ) { return false; } } return true; } return false; } /** * Rough area in square kilometres (equirectangular projection, holes subtracted). Used only to rank zones. * * @param array $geo Polygon or MultiPolygon. * @return float */ public static function area_km2( array $geo ) { if ( ! isset( $geo['type'], $geo['coordinates'] ) ) { return 0.0; } if ( 'Polygon' === $geo['type'] ) { return self::polygon_area( $geo['coordinates'] ); } $total = 0.0; if ( 'MultiPolygon' === $geo['type'] ) { foreach ( $geo['coordinates'] as $polygon ) { $total += self::polygon_area( $polygon ); } } return $total; } /** * Validate polygon rings. * * @param array $rings Rings. * @return bool */ private static function valid_polygon( array $rings ) { if ( empty( $rings ) ) { return false; } foreach ( $rings as $ring ) { if ( ! is_array( $ring ) || count( $ring ) < 4 ) { return false; } foreach ( $ring as $pos ) { if ( ! is_array( $pos ) || count( $pos ) < 2 || ! is_numeric( $pos[0] ) || ! is_numeric( $pos[1] ) ) { return false; } if ( abs( (float) $pos[0] ) > 180 || abs( (float) $pos[1] ) > 90 ) { return false; } } } return true; } /** * Area of one polygon (outer minus holes). * * @param array $rings Rings. * @return float */ private static function polygon_area( array $rings ) { $area = 0.0; foreach ( $rings as $i => $ring ) { $a = self::ring_area( $ring ); $area += ( 0 === $i ) ? $a : -$a; } return max( 0.0, $area ); } /** * Shoelace area of a ring in km2. * * @param array $ring Ring. * @return float */ private static function ring_area( array $ring ) { $n = count( $ring ); if ( $n < 3 ) { return 0.0; } $mean_lat = 0.0; foreach ( $ring as $pos ) { $mean_lat += (float) $pos[1]; } $mean_lat /= $n; $kx = 111.320 * cos( deg2rad( $mean_lat ) ); $ky = 110.574; $sum = 0.0; for ( $i = 0, $j = $n - 1; $i < $n; $j = $i++ ) { $sum += ( (float) $ring[ $j ][0] * $kx ) * ( (float) $ring[ $i ][1] * $ky ) - ( (float) $ring[ $i ][0] * $kx ) * ( (float) $ring[ $j ][1] * $ky ); } return abs( $sum ) / 2; } /** * Is the point on any edge of the ring? * * @param float $lon Longitude. * @param float $lat Latitude. * @param array $ring Ring. * @return bool */ private static function on_ring_edge( $lon, $lat, array $ring ) { for ( $i = 0, $n = count( $ring ), $j = $n - 1; $i < $n; $j = $i++ ) { if ( self::on_segment( $lon, $lat, (float) $ring[ $i ][0], (float) $ring[ $i ][1], (float) $ring[ $j ][0], (float) $ring[ $j ][1] ) ) { return true; } } return false; } /** * Is (px,py) on the segment (ax,ay)-(bx,by)? Tolerance is about 1e-9 degrees. * * @param float $px Point x. * @param float $py Point y. * @param float $ax Segment start x. * @param float $ay Segment start y. * @param float $bx Segment end x. * @param float $by Segment end y. * @return bool */ private static function on_segment( $px, $py, $ax, $ay, $bx, $by ) { $eps = 1e-9; $cross = ( $px - $ax ) * ( $by - $ay ) - ( $py - $ay ) * ( $bx - $ax ); if ( abs( $cross ) > $eps ) { return false; } return $px >= min( $ax, $bx ) - $eps && $px <= max( $ax, $bx ) + $eps && $py >= min( $ay, $by ) - $eps && $py <= max( $ay, $by ) + $eps; } }