DEV Community

Sahil kashyap
Sahil kashyap

Posted on

3 3

Turf.js to php

Problem: Turfjs works well for client-side but I wanted to use it in server-side
Recently I had to check if a point lies inside of the polygon.

I was using spatial data in mysql. Mysql provides way(ST_Contains and some other ways) to check if a point lies in a polygon. But unfortunately it was not scanning all the rows.

I found turf.js was able to do this properly:
Here's I have used this with mapbox:https://sahilkashyap64.github.io/USA-zipcode-boundary/mapbox+turf2.html

var point = turf.point([lon, lat]);
const PointwithinFeatureCollection = (point) => {
    var ptsWithin, found = false;
    var zip = false;
    turf.featureEach(data, function (currentFeature, featureIndex) {

        var geom = turf.getType(currentFeature)
        console.log('geom', geom);
        if (geom == 'MultiPolygon') {

            let coordinates = turf.multiPolygon(currentFeature.geometry.coordinates);

            ptsWithin = turf.booleanPointInPolygon(point, coordinates);
            console.log("Found in Multipolygon: : ", ptsWithin);
            if (ptsWithin) {
                zip = currentFeature.properties.title;
            }


        } else if (geom == 'Polygon') {
            let coordinates = turf.polygon(currentFeature.geometry.coordinates);
            found = turf.booleanPointInPolygon(point, coordinates);
            console.log('found', found);
            if (found) {
                zip = currentFeature.properties.title;
            }
        }
    });
    if (zip === false) {
        return {
            "success": false,
            "message": "Not within zipcode boundary",
            "response_code": 403
        };
    } else {
        return {
            "success": true,
            "data": zip,
            "message": zip + " Zipcode is allowed.",
            "response_code": 200
        };

    }
};
PointwithinFeatureCollection(point);

HERE'S THE PHP CODE:

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Zipcode;
use Illuminate\Support\Facades\DB;
class TurfController extends Controller
{
/**
* Wraps a GeoJSON {@link Geometry} in a GeoJSON {@link Feature}.
*
* @name feature
* @param {Geometry} geometry input geometry
* @param {Object} properties properties
* @returns {FeatureCollection} a FeatureCollection of input features
* @example
* var geometry = {
* "type": "Point",
* "coordinates": [
* 67.5,
* 32.84267363195431
* ]
* }
*
* var feature = turf.feature(geometry);
*
* //=feature
*/
function feature( $geometry, $properties ) {
return [
'type' => 'Feature',
'properties' => $properties || [],
'geometry' => $geometry
];
}
/**
* Takes coordinates and properties (optional) and returns a new {@link Point} feature.
*
* @name point
* @param {number[]} coordinates longitude, latitude position (each in decimal degrees)
* @param {Object=} properties an Object that is used as the {@link Feature}'s
* properties
* @returns {Feature<Point>} a Point feature
* @example
* var pt1 = turf.point([-75.343, 39.984]);
*
* //=pt1
*/
function point( $coordinates, $properties ) {
if ( !is_array( $coordinates ) ) { throw new \Exception( 'Coordinates must be an array' ); }
if ( count( $coordinates ) < 2 ) { throw new \Exception( 'Coordinates must be at least 2 numbers long' ); }
return $this->feature( [
'type' => 'Point',
'coordinates' => array_slice( $coordinates, 0 )
], $properties
);
}
public function index(Request $request)
{
$LAT= $request['latitude'];
$LON = $request['longitude'];
$point = $this->point([$LON, $LAT],'');
$data = $this->data();
// $point = $this->featureEach($data,);
$answer=$this->PointwithinFeatureCollection($point);
if($answer['response_code']==403){
return response()->json($answer, 403);
}else{
return response()->json($answer, 200);}
}
/**
* Callback for featureEach
*
* @callback featureEachCallback
* @param {Feature<any>} currentFeature The current Feature being processed.
* @param {number} featureIndex The current index of the Feature being processed.
*/
/**
* Iterate over features in any GeoJSON object, similar to
* Array.forEach.
*
* @name featureEach
* @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON object
* @param {Function} callback a method that takes (currentFeature, featureIndex)
* @example
* var features = turf.featureCollection([
* turf.point([26, 37], {foo: 'bar'}),
* turf.point([36, 53], {hello: 'world'})
* ]);
*
* turf.featureEach(features, function (currentFeature, featureIndex) {
* //=currentFeature
* //=featureIndex
* });
*/
function featureEach( $geojson, $callback ) {
if ( $geojson['type'] === 'Feature' ) {
$callback( $geojson, 0 );
} elseif ( $geojson['type'] === 'FeatureCollection' ) {
for ( $i = 0; $i < count( $geojson['features'] ); $i++ ) {
$callback( $geojson['features'][ $i ], $i );
}
}
}
function data(){
$data = Zipcode::select('zipcodes.*',DB::raw("ST_AsGeoJSON(map.`SHAPE`) shape"))->where('zipcodes.status',1)->join('map', 'zipcodes.zipcode', '=', 'map.zcta5ce10')->get();
# Build GeoJSON feature collection array
$geojson = array(
'type' => 'FeatureCollection',
// 'crs' => htmlspecialchars(json_encode($crs), ENT_QUOTES, 'UTF-8'),
'features' => array()
);
if($data->isEmpty()){
$response = [
'success' => false,
'data' => $data,
'message'=>'No zipcode is allowed',
'response_code'=>200,
];
return response()->json($response, 200);
}
foreach($data as $fieldnam) {
$properties = array(
'color'=> 'red',
'title' => $fieldnam['zipcode']
);
$feature = array(
'type' => 'Feature',
'properties' => $properties,
'geometry' => $fieldnam->shape,
);
array_push($geojson['features'], $feature);
}
return $geojson;
}
function PointwithinFeatureCollection($point){
$mydata = $this->data();
global $zip;
$ptsWithin = null; $found = false; $zip='';
$this->featureEach( $mydata, function ( $currentFeature, $featureIndex ) use ( &$turf, $point,$zip,$ptsWithin) {
global $zip;
$geom = $this->getType( $currentFeature,'' );
if ( $geom == 'MultiPolygon' ) {
$multiPolygon = $this->multiPolygon( $currentFeature['geometry']['coordinates'],'' );
$ptsWithin = $this->booleanPointInPolygon( $point, $multiPolygon );
if ( $ptsWithin===true ) {
$zip = $currentFeature['properties']['title'];
}
} else if ( $geom == 'Polygon' ) {
$polygon = $this->polygon( $currentFeature['geometry']['coordinates'],'','' );
$found = $this->booleanPointInPolygon( $point, $polygon );
if ( $found ===true) {
$zip = $currentFeature['properties']['title'];
}
}
}
);
if (empty ( $zip ) ) {
return [
'success' => false,
'message' => 'Not within zipcode boundary',
'response_code' => 403
];
} else {
return [
'success' => true,
'data' => $zip,
'message' => $zip . ' Zipcode is allowed.',
'response_code' => 200
];
}
}
/**
* Get GeoJSON object's type, Geometry type is prioritize.
*
* @param {GeoJSON} geojson GeoJSON object
* @param {string} [name="geojson"] name of the variable to display in error message
* @returns {string} GeoJSON type
* @example
* var point = {
* "type": "Feature",
* "properties": {},
* "geometry": {
* "type": "Point",
* "coordinates": [110, 40]
* }
* }
* var geom = turf.getType(point)
* //="Point"
*/
function getType( $geojson, $name ) {
if ( $geojson['type'] === 'FeatureCollection' ) {
return 'FeatureCollection';
}
if ( $geojson['type'] === 'GeometryCollection' ) {
return 'GeometryCollection';
}
if ( $geojson['type'] === 'Feature' && $geojson['geometry'] !== null ) {
return $geojson['geometry']['type'];
}
return $geojson['type'];
}
/**
* Creates a {@link Feature<MultiPolygon>} based on a
* coordinate array. Properties can be added optionally.
*
* @name multiPolygon
* @param {Array<Array<Array<Array<number>>>>} coordinates an array of Polygons
* @param {Object=} properties an Object of key-value pairs to add as properties
* @returns {Feature<MultiPolygon>} a multipolygon feature
* @throws {\Exception} if no coordinates are passed
* @example
* var multiPoly = turf.multiPolygon([[[[0,0],[0,10],[10,10],[10,0],[0,0]]]);
*
* //=multiPoly
*
*/
function multiPolygon( $coordinates, $properties ) {
if ( !$coordinates ) {
throw new \Exception( 'No coordinates passed' );
}
return $this->feature( [
'type' => 'MultiPolygon',
'coordinates' => $coordinates
], $properties
);
}
/**
* Creates a {@link Polygon} {@link Feature} from an Array of LinearRings.
*
* @name polygon
* @param {Array<Array<Array<number>>>} coordinates an array of LinearRings
* @param {Object} [properties={}] an Object of key-value pairs to add as properties
* @param {Object} [options={}] Optional Parameters
* @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature
* @param {string|number} [options.id] Identifier associated with the Feature
* @returns {Feature<Polygon>} Polygon Feature
* @example
* var polygon = turf.polygon([[[-5, 52], [-4, 56], [-2, 51], [-7, 54], [-5, 52]]], { name: 'poly1' });
*
* //=polygon
*/
function polygon( $coordinates, $properties, $options ) {
if ( !$coordinates ) { throw new \Exception( 'coordinates is required' ); }
for ( $i = 0; $i < count( $coordinates ); $i++ ) {
$ring = $coordinates[ $i ];
if ( count( $ring ) < 4 ) {
throw new \Exception( 'Each LinearRing of a Polygon must have 4 or more Positions.' );
}
for ( $j = 0; $j < count( $ring[ count( $ring ) - 1 ] ); $j++ ) {
// Check if first point of Polygon contains two numbers
if ( $i === 0 && $j === 0 && !$this->isNumber( $ring[ 0 ][ 0 ] ) || !$this->isNumber( $ring[ 0 ][ 1 ] ) ) { throw new \Exception( 'coordinates must contain numbers' ); }
if ( $ring[ count( $ring ) - 1 ][ $j ] !== $ring[ 0 ][ $j ] ) {
throw new \Exception( 'First and last Position are not equivalent.' );
}
}
}
return $this->feature( [
'type' => 'Polygon',
'coordinates' => $coordinates
], $properties, $options
);
}
function isNumber( $num ) {
return is_numeric($num) && !is_nan(floatval($num));
}
/**
* Takes a {@link Point} and a {@link Polygon} or {@link MultiPolygon} and determines if the point resides inside the polygon. The polygon can
* be convex or concave. The function accounts for holes.
*
* @name booleanPointInPolygon
* @param {Coord} point input point
* @param {Feature<Polygon|MultiPolygon>} polygon input polygon or multipolygon
* @param {Object} [options={}] Optional parameters
* @param {boolean} [options.ignoreBoundary=false] True if polygon boundary should be ignored when determining if the point is inside the polygon otherwise false.
* @returns {boolean} `true` if the Point is inside the Polygon; `false` if the Point is not inside the Polygon
* @example
* var pt = turf.point([-77, 44]);
* var poly = turf.polygon([[
* [-81, 41],
* [-81, 47],
* [-72, 47],
* [-72, 41],
* [-81, 41]
* ]]);
*
* turf.booleanPointInPolygon(pt, poly);
* //= true
*/
function booleanPointInPolygon( $point, $polygon ) {
// Optional parameters
// $options = $options || [];
// if ( gettype( $options ) !== 'object' ) { throw new \Exception( 'options is invalid' ); }
$ignoreBoundary = true;
// validation
if ( !$point ) { throw new \Exception( 'point is required' ); }
if ( !$polygon ) { throw new \Exception( 'polygon is required' ); }
// echo "from booleanpointInpolygon";
// print_r($point);
$pt = $this->getCoord( $point );
$polys = $this->getCoords( $polygon );
$type = ( $polygon['geometry'] ) ? $polygon['geometry']['type'] : $polygon['type'];
if(array_key_exists('bbox', $polygon)) {
$bbox = $polygon['bbox'];
// Quick elimination if point is not inside bbox
if ( $bbox && inBBox( $pt, $bbox ) === false ) { return false; }
}
// normalize to multipolygon
if ( $type === 'Polygon' ) { $polys = [ $polys ]; }
for ( $i = 0, $insidePoly = false; $i < count( $polys ) && !$insidePoly; $i++ ) {
// check if it is in the outer ring first
if ( $this->inRing( $pt, $polys[ $i ][ 0 ], $ignoreBoundary ) ) {
$inHole = false;
$k = 1;
// check for the point in any of the holes
while ( $k < count( $polys[ $i ] ) && !$inHole ) {
if ( $this->inRing( $pt, $polys[ $i ][ $k ], !$ignoreBoundary ) ) {
$inHole = true;
}
$k++;
}
if ( !$inHole ) { $insidePoly = true; }
}
}
return $insidePoly;
}
/**
* Unwrap a coordinate from a Point Feature, Geometry or a single coordinate.
*
* @name getCoord
* @param {Array<number>|Geometry<Point>|Feature<Point>} obj Object
* @returns {Array<number>} coordinates
* @example
* var pt = turf.point([10, 10]);
*
* var coord = turf.getCoord(pt);
* //= [10, 10]
*/
function getCoord( $obj ) {
if ( !$obj ) { throw new \Exception( 'obj is required' ); }
$coordinates = $this->getCoords( $obj );
// getCoord() must contain at least two numbers (Point)
if ( count( $coordinates ) > 1 && $this->isNumber( $coordinates[ 0 ] ) && $this->isNumber( $coordinates[ 1 ] ) ) {
return $coordinates;
} else {
throw new \Exception( 'Coordinate is not a valid Point' );
}
}
/**
* Unwrap coordinates from a Feature, Geometry Object or an Array of numbers
*
* @name getCoords
* @param {Array<number>|Geometry|Feature} obj Object
* @returns {Array<number>} coordinates
* @example
* var poly = turf.polygon([[[119.32, -8.7], [119.55, -8.69], [119.51, -8.54], [119.32, -8.7]]]);
*
* var coord = turf.getCoords(poly);
* //= [[[119.32, -8.7], [119.55, -8.69], [119.51, -8.54], [119.32, -8.7]]]
*/
function getCoords( $obj ) {
if ( !$obj ) { throw new \Exception( 'obj is required' ); }
$coordinates = null;
if ( $obj['geometry'] && $obj['geometry']['coordinates'] ) {
$coordinates = $obj['geometry']['coordinates'];
}
// Checks if coordinates contains a number
if ( $coordinates ) {
$this->containsNumber( $coordinates );
return $coordinates;
}
throw new \Exception( 'No valid coordinates' );
}
/**
* Checks if coordinates contains a number
*
* @name containsNumber
* @param {Array<any>} coordinates GeoJSON Coordinates
* @returns {boolean} true if Array contains a number
*/
function containsNumber( $coordinates ) {
if ( count( $coordinates ) > 1 && $this->isNumber( $coordinates[ 0 ] ) && $this->isNumber( $coordinates[ 1 ] ) ) {
return true;
}
if ( is_array( $coordinates[ 0 ] ) && count( $coordinates[ 0 ] ) ) {
return $this->containsNumber( $coordinates[ 0 ] );
}
throw new \Exception( 'coordinates must only contain numbers' );
}
function getGeom( $geojson ) {
if ( $geojson['type'] === 'Feature' ) {
return $geojson['geometry'];
}
return $geojson;
}
/**
* inBBox
*
* @private
* @param {Position} pt point [x,y]
* @param {BBox} bbox BBox [west, south, east, north]
* @returns {boolean} true/false if point is inside BBox
*/
function inBBox( $pt, $bbox ) {
return $bbox[ 0 ] <= $pt[ 0 ]
&& $bbox[ 1 ] <= $pt[ 1 ]
&& $bbox[ 2 ] >= $pt[ 0 ]
&& $bbox[ 3 ] >= $pt[ 1 ];
}
/**
* inRing
*
* @private
* @param {Array<number>} pt [x,y]
* @param {Array<Array<number>>} ring [[x,y], [x,y],..]
* @param {boolean} ignoreBoundary ignoreBoundary
* @returns {boolean} inRing
*/
function inRing( $pt, $ring, $ignoreBoundary ) {
$isInside = false;
if ( $ring[ 0 ][ 0 ] === $ring[ count( $ring ) - 1 ][ 0 ] && $ring[ 0 ][ 1 ] === $ring[ count( $ring ) - 1 ][ 1 ] ) {
$ring = array_slice( $ring, 0, count( $ring ) - 1/*CHECK THIS*/ );
}
for ( $i = 0, $j = count( $ring ) - 1; $i < count( $ring ); $j = $i++ ) {
$xi = $ring[ $i ][ 0 ];
$yi = $ring[ $i ][ 1 ];
$xj = $ring[ $j ][ 0 ];
$yj = $ring[ $j ][ 1 ];
$onBoundary = ( $pt[ 1 ] * ( $xi - $xj ) + $yi * ( $xj - $pt[ 0 ] ) + $yj * ( $pt[ 0 ] - $xi ) === 0 )
&& ( ( $xi - $pt[ 0 ] ) * ( $xj - $pt[ 0 ] ) <= 0 ) && ( ( $yi - $pt[ 1 ] ) * ( $yj - $pt[ 1 ] ) <= 0 );
if ( $onBoundary ) {
return !$ignoreBoundary;
}
$intersect = ( ( $yi > $pt[ 1 ] ) !== ( $yj > $pt[ 1 ] ) )
&& ( $pt[ 0 ] < ( $xj - $xi ) * ( $pt[ 1 ] - $yi ) / ( $yj - $yi ) + $xi );
if ( $intersect ) {
$isInside = !$isInside;
}
}
return $isInside;
}
}



Here's what I did to convert this code in php:-

1)I manually wrote all the turf function I was using.

2) Extracted those methods from the js file and wrote them in seperate js file. eg: point ,getType,featureEach,multiPolygon,polygon,booleanPointInPolygon I found these function is turf/helper,turf/invariant,turf/meta

  1. I used "npm install -g javascript-to-php" and then this js2php myfile.js > myfile.php

Note:You will have to manually edit some generated code and If you try to convert whole turf index.js sometimes it generates error

Retry later

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more